2012 Marakana, Inc. All rights reserved.
Last Updated: 2012-10-20
The purpose of this module is to explain the anatomy of the Android OS. We will walk through the Android stack, starting from the bottom and moving up:
| Android Version | Kernel Version |
|---|---|
1.0 |
2.6.25 |
1.5 |
2.6.27 |
1.6 |
2.6.29 |
2.2 |
2.6.32 |
2.3 |
2.6.35 |
3.0 |
2.6.36 |
4.0.1 |
3.0.1 |
4.0.3 |
3.0.8 |
4.1.1 |
3.0.31 (GN), 3.1.10 (N7) |
|
|
Significant efforts have been made by the Linux community to move most of the Android-specific changes back into the mainline Linux kernel (3.3 and 3.5), with a hope that Android could then switch to the official kernel. |
| Android Version | Kernel Version |
|---|---|
kernel/common.git |
The "official" Android Kernel branch (used as the basis for others) |
kernel/goldfish.git |
Kernel tree for the "goldfish" emulator |
kernel/msm.git |
Kernel tree for ADP1, ADP2, Nexus One, and other Qualcomm MSM SoCs |
kernel/omap.git |
Kernel tree for PandaBoard and Galaxy Nexus and other TI OMAP SoCs |
kernel/samsung.git |
Kernel tree for Nexus S and other Samsung Hummingbird SoCs |
kernel/tegra.git |
Kernel tree for Xoom and Nexus 7 and other NVIDIA Tegra SoCs |
kernel/exynos.git |
Kernel tree for Samsung Exynos SoCs |
The following are some of the changes/additions Android makes to the Linux kernel.
int size = 4096; int fd = ashmem_create_region("MySharedRegionName", size); if (fd == 0) { data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if(data != MAP_FAILED) { /* for security reasons, no other process can ashmem_create_region() with the same name */ /* instead, to share this memory, we send fd via Binder IPC to another process */ /* that process then mmap()'s it the same way in order to access the shared memory */ } }
#include <linux/wakelock.h> wake_lock_init(struct wakelock *lock, int type, const char *name); void wake_lock(struct wake_lock *lock); void wake_unlock(struct wake_lock *lock); void wake_lock_timeout(struct wake_lock *lock, long timeout); void wake_lock_destroy(struct wake_lock *lock); int wake_lock_active(struct wake_lock *lock); long has_wake_lock(int type);
echo "MyLock" > /sys/power/wake_lock echo "MyLock" > /sys/power/wake_unlock
| Flag | CPU | Screen | Keybaord |
|---|---|---|---|
PARTIAL_WAKE_LOCK |
On |
Off |
Off |
SCREEN_DIM_WAKE_LOCK |
On |
Dim |
Off |
SCREEN_BRIGHT_WAKE_LOCK |
On |
Bright |
Off |
FULL_WAKE_LOCK |
On |
Bright |
On |
| Flag | Notes |
|---|---|
ACQUIRE_CAUSES_WAKEUP |
Force screen to turn on as soon as the wake-lock is acquired (i.e. don’t wait for user activity) |
ON_AFTER_RELEASE |
When set, the user activity timer will be reset when the wake-lock is released, causing the screen/keyboard illumination to remain on a bit longer (reduces flicker if code is cycling between wake-locks) |
# Define the oom_adj values for the classes of processes that can be
# killed by the kernel. These are used in ActivityManagerService.
setprop ro.FOREGROUND_APP_ADJ 0
setprop ro.VISIBLE_APP_ADJ 1
setprop ro.PERCEPTIBLE_APP_ADJ 2
setprop ro.HEAVY_WEIGHT_APP_ADJ 3
setprop ro.SECONDARY_SERVER_ADJ 4
setprop ro.BACKUP_APP_ADJ 5
setprop ro.HOME_APP_ADJ 6
setprop ro.HIDDEN_APP_MIN_ADJ 7
setprop ro.EMPTY_APP_ADJ 15
# Define the memory thresholds at which the above process classes will
# be killed. These numbers are in pages (4k).
setprop ro.FOREGROUND_APP_MEM 2048
setprop ro.VISIBLE_APP_MEM 3072
setprop ro.PERCEPTIBLE_APP_MEM 4096
setprop ro.HEAVY_WEIGHT_APP_MEM 4096
setprop ro.SECONDARY_SERVER_MEM 6144
setprop ro.BACKUP_APP_MEM 6144
setprop ro.HOME_APP_MEM 6144
setprop ro.HIDDEN_APP_MEM 7168
setprop ro.EMPTY_APP_MEM 8192
# Write value must be consistent with the above properties.
# Note that the driver only supports 6 slots, so we have combined some of
# the classes into the same memory level; the associated processes of higher
# classes will still be killed first.
write /sys/module/lowmemorykiller/parameters/adj 0,1,2,4,7,15
write /proc/sys/vm/overcommit_memory 1
write /proc/sys/vm/min_free_order_shift 4
write /sys/module/lowmemorykiller/parameters/minfree 2048,3072,4096,6144,7168,8192
# Set init its forked children's oom_adj.
write /proc/1/oom_adj -16
Foreground processes - with an Activity that just ran onResume(), or a Service bound to it or started as foreground, or executing its callback methods, or a BroadcastReceiver executing onReceive()
Visible processes - with an Activity that just ran onPause() but is still visible or a Service bound to a component from a visible process
Service processes - with a Service that has been started with Context.startService()
Background processes - with an Activity that just ran onStop()
Empty processes - with no components (kept around just for caching purposes)
... #define AID_NET_BT_ADMIN 3001 #define AID_NET_BT 3002 #define AID_INET 3003 ...
... #include <linux/android_aid.h> static inline int current_has_network(void) { return in_egroup_p(AID_INET) || capable(CAP_NET_RAW); } ... static int inet_create(struct net *net, struct socket *sock, int protocol) { ... if (!current_has_network()) return -EACCES; ... } ...
... #define AID_NET_BT_ADMIN 3001 /* bluetooth: create any socket */ #define AID_NET_BT 3002 /* bluetooth: create sco, rfcomm or l2cap sockets */ #define AID_INET 3003 /* can create AF_INET and AF_INET6 sockets */ ... static const struct android_id_info android_ids[] = { ... { "net_bt_admin", AID_NET_BT_ADMIN, }, { "net_bt", AID_NET_BT, }, ... { "inet", AID_INET, }, ... }; ...
<permissions> ... <permission name="android.permission.BLUETOOTH_ADMIN" > <group gid="net_bt_admin" /> </permission> <permission name="android.permission.BLUETOOTH" > <group gid="net_bt" /> </permission> <permission name="android.permission.INTERNET" > <group gid="inet" /> </permission> ... </permissions>
write /proc/sys/kernel/panic_on_oops 1 write /proc/sys/kernel/hung_task_timeout_secs 0 write /proc/cpu/alignment 4 write /proc/sys/kernel/sched_latency_ns 10000000 write /proc/sys/kernel/sched_wakeup_granularity_ns 2000000 write /proc/sys/kernel/sched_compat_yield 1 write /proc/sys/kernel/sched_child_runs_first 0
<*_HARDWARE_MODULE_ID>.<ro.product.board>.so
<*_HARDWARE_MODULE_ID>.<ro.board.platform>.so
<*_HARDWARE_MODULE_ID>.<ro.arch>.so
<*_HARDWARE_MODULE_ID>.default.so
dev_mount sdcard /mnt/sdcard auto /devices/platform/goldfish_mmc.0 /devices/platform/msm_sdcc.2/mmc_host/mmc1
dev_mount sdcard /mnt/sdcard 3 /devices/platform/s3c-sdhci.0/mmc_host/mmc0/mmc0:0001/block/mmcblk0
... /dev/urandom 0666 root root /dev/ashmem 0666 root root /dev/binder 0666 root root ... /dev/log/* 0662 root log ... /dev/ttyMSM0 0600 bluetooth bluetooth ... /dev/alarm 0664 system radio ... /dev/cam 0660 root camera ... /dev/akm8976_pffd 0640 compass system /dev/lightsensor 0640 system system ... /dev/bus/usb/* 0660 root usb /sys/devices/virtual/input/input* enable 0660 root input ...
What are the four layers of the Android Stack?
What is the purpose of the Linux Kernel? How does Android use it?
What’s missing in Android for it to be considered a more traditional Linux distribution?
How is the Linux kernel on Android different?
Name at least five Android Linux kernel extensions.
What is Binder?
What does Binder do and why do we need it?
How is Binder exposed to the user-space?
How does Binder compare to other similar mechanisms?
What is Ashmem?
Why do we need Ashmem?
How do applications use Ashmem?
How does Ashmem compare to other similar mechanisms?
How does the power management on Android compare to traditional Linux distributions?
What are wake locks?
Where in the stack are the wake locks used?
How do applications get access to wake locks?
What is the purpose of early suspend feature of the Linux kernel?
What is the purpose of the alarm driver?
What are the two opposing objectives on Android when it comes with memory management?
What is Android’s first line of defense when it comes to memory management?
What is the low-memory-killer and what does it do?
What are the relative levels of priority of application processes on Android?
How does the low-memory-killer know who to kill and when?
How can applications avoid being low-memory-killed?
What is the purpose of the logger on Android?
Name at least three log destinations.
Where is the log information stored?
What is the purpose of the CONFIG_ANDROID_PARANOID_NETWORK kernel option on Android?
Name at least three sub-layers of the native layer.
What is Bionic, why do we need it, and how does it differ from its alternatives?
What are the two main purposes of user-space HAL on Android?
How is the user-space HAL on Android exposed to the layers above it?
Name at least three classes of devices exposed by user-space HAL.
Name at least five native daemons and explain what they do.
What is the purpose of ueventd on Android?
What are the two "flingers" on Android?
Name at least three "function" libraries on Android.
What is the name of the Android’s media framework?
What is the purpose of Dalvik on Android?
How does Dalvik differ from its alternatives?
What is the purpose of zygote?
What was added to Dalvik in Froyo (Android 2.2)?
What’s inside the Android Application Framework layer?
What is the purpose of system services on Android? What value do they add?
What is the purpose of managers for system services?
What is the purpose of servicemanager daemon?
Name at least five system services on Android.
What does ActivityManagerService do?
What does PackageManagerService do?
What does the PowerManagerService do?
What does the AlarmManagerService do?
What does the KeyguardManagerService do?
What does the InputMethodManagerService do?
What is Android CDD and why do we need it?
What’s inside an APK?
What’s the difference between system and non-system apps?
What are the different classes of applications that ship with Android?
Name at least three "special" applications on Android?
Android is put together of about equal part Java and C. So, no wonder that we need an easy way to bridge between these two totally different worlds. Java offers Java Native Interface (JNI) as a framework connecting the world of Java to the native code. Android goes a step further by packaging other useful tools and libraries into a Native Development Kit, or NDK. NDK makes developing C/C++ code that works with an Android app much simpler than if one was to do it by hand. Topics covered include:
package com.marakana.android.fetchurl; public class FetchUrl { public static native byte[] fetch(String url); static { System.loadLibrary("fetchurl"); } }
In this module, we’ll explore the following topics:
JNI is an interface that allows Java to interact with code written in another language
Motivation for JNI:
|
|
JNI code is not portable! |
|
|
JNI can also be used to invoke Java code from within natively-written applications - such as those written in C/C++. In fact, the java command-line utility is an example of one such application, that launches Java code in a Java Virtual Machine. |
We start by creating a Java class with one or more native methods
package com.marakana.jniexamples; public class Hello { public static native void sayHi(String who, int times); //static { System.loadLibrary("hello"); //
} public static void main(String[] args) { sayHi(args[0], Integer.parseInt(args[1])); //
} }
| The method sayHi(String, int) will be implemented in C/C+ in separate files, which will be compiled into a shared library. | |
| Load the shared library by its logical name. The actual name is system-dependent: libhello.so (on Linux/Unix), hello.dll (on Windows), and libhello.jnilib (Mac OSX). | |
| Here we call our native method as a regular Java method. |
Compile the Java code
$ mkdir -p bin $ javac -d bin/ src/com/marakana/jniexamples/Hello.java
Using the javah tool, we generate the C header file from the compiled com.marakana.jniexamples.Hello class:
$ mkdir -p jni $ javah -jni -classpath bin -d jni com.marakana.jniexamples.Hello
Observe the generated C header file:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_marakana_jniexamples_Hello */ #ifndef _Included_com_marakana_jniexamples_Hello #define _Included_com_marakana_jniexamples_Hello #ifdef __cplusplus extern "C" { #endif /* * Class: com_marakana_jniexamples_Hello * Method: sayHi * Signature: (Ljava/lang/String;I)V */ JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Hello_sayHi (JNIEnv *, jclass, jstring, jint); #ifdef __cplusplus } #endif #endif
|
|
Method names resolve to C functions based on a pre-defined naming strategy: the prefix Java_, followed by a mangled fully-qualified class name, followed by an underscore ("_") separator, followed by a mangled method name. For overloaded native methods, two underscores ("__") followed by the mangled argument signature. |
Provide the C implementation:
#include "com_marakana_jniexamples_Hello.h" JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Hello_sayHi (JNIEnv *env, jclass clazz, jstring who, jint times) { const char *name = (*env)->GetStringUTFChars(env, who, NULL); if (name != NULL) { jint i; for (i = 0; i < times; i++) { printf("Hello %s\n", name); } (*env)->ReleaseStringUTFChars(env, who, name); } }
|
|
Most of the time, we cannot just use Java data types directly in C. For example, we have to convert java.lang.String to char * before we can effectively use it in C. |
|
|
This code assumes: #define NULL ((void *) 0) |
Compile the shared library
$ mkdir -p libs $ gcc -o libs/libhello.jnilib -lc -shared \ -I/System/Library/Frameworks/JavaVM.framework/Headers \ jni/com_marakana_jniexamples_Hello.c $ file libs/libhello.jnilib libs/libhello.jnilib: Mach-O 64-bit dynamically linked shared library x86_64
|
|
On Unix/Linux, compile as: gcc -o libs/libhello.so -lc -shared -fPIC -I$JAVA_HOME/include jni/com_marakana_jniexamples_Hello.c |
Run our code
$ java -Djava.library.path=libs -classpath bin com.marakana.jniexamples.Hello Student 5 Hello Student Hello Student Hello Student Hello Student Hello Student
|
|
Instead of specifying -Djava.library.path=libs, we could have preceded our java command with export LD_LIBRARY_PATH=libs. |
|
|
Common mistakes resulting in java.lang.UnsatisfiedLinkError usually come from incorrect naming of the shared library (O/S-dependent), the library not being in the search path, or wrong library being loaded by Java code. |
| Java Language Type | Native Type | Description | typedef (C99) | typedef (otherwise) |
|---|---|---|---|---|
boolean |
jboolean |
unsigned 8 bits |
uint8_t |
unsigned char |
byte |
jbyte |
signed 8 bits |
int8_t |
signed char |
char |
jchar |
unsigned 16 bits |
uint16_t |
unsigned short |
short |
jshort |
signed 16 bits |
int16_t |
short |
int |
jint |
signed 32 bits |
int32_t |
int |
long |
jlong |
signed 64 bits |
int64_t |
long long |
float |
jfloat |
32-bit IEEE 754 |
float |
float |
double |
jdouble |
64-bit IEEE 754 |
double |
double |
void |
void |
N/A |
N/A |
N/A |
N/A |
jsize |
used to describe cardinal indices and sizes |
jint |
jint |
| Java Boolean Value | Native Boolean Type | Definition |
|---|---|---|
false |
JNI_FALSE |
#define JNI_FALSE 0 |
true |
JNI_TRUE |
#define JNI_TRUE 1 |
| Java Language Type | Native Type | typedef in C |
|---|---|---|
java.lang.Object |
jobject |
void* |
java.lang.Class |
jclass |
jobject |
java.lang.Throwable |
jthrowable |
jobject |
java.lang.String |
jstring |
jobject |
java.lang.ref.WeakReference |
jweak |
jobject |
N/A |
jarray |
jobject |
java.lang.Object[] |
jobjectArray |
jarray |
boolean[] |
jbooleanArray |
jarray |
byte[] |
jbyteArray |
jarray |
char[] |
jcharArray |
jarray |
short[] |
jshortArray |
jarray |
int[] |
jintArray |
jarray |
long[] |
jlongArray |
jarray |
float[] |
jfloatArray |
jarray |
double[] |
jdoubleArray |
jarray |
|
|
A note about reference types in C++
These reference types in C++ are defined as proper classes: class _jobject {}; class _jclass : public _jobject {}; class _jstring : public _jobject {}; class _jarray : public _jobject {}; class _jobjectArray : public _jarray {}; class _jbooleanArray : public _jarray {}; class _jbyteArray : public _jarray {}; class _jcharArray : public _jarray {}; class _jshortArray : public _jarray {}; class _jintArray : public _jarray {}; class _jlongArray : public _jarray {}; class _jfloatArray : public _jarray {}; class _jdoubleArray : public _jarray {}; class _jthrowable : public _jobject {}; typedef _jobject* jobject; typedef _jclass* jclass; typedef _jstring* jstring; typedef _jarray* jarray; typedef _jobjectArray* jobjectArray; typedef _jbooleanArray* jbooleanArray; typedef _jbyteArray* jbyteArray; typedef _jcharArray* jcharArray; typedef _jshortArray* jshortArray; typedef _jintArray* jintArray; typedef _jlongArray* jlongArray; typedef _jfloatArray* jfloatArray; typedef _jdoubleArray* jdoubleArray; typedef _jthrowable* jthrowable; typedef _jobject* jweak; |
|
|
A tip about NULL in Android NDK
Android’s native development kit (NDK) does not define NULL in its jni.h, so the following definition can be useful when working with pointers and reference types: #define NULL ((void *) 0) |
|
|
Opaque references are C pointer types that refer to internal data structures in the JVM. It is an error to try to dereference opaque references and try to use them directly. |
void DeleteLocalRef(JNIEnv *env, jobject localRef);
|
|
In practice, we only really want to use DeleteLocalRef(JNIEnv *, jobject) if we know we are not going to need the reference in the rest of the function body, say before we start executing code that may take a long time. This allows the referenced memory to be freed by GC, assuming nobody else is using it. |
jint EnsureLocalCapacity(JNIEnv *env, jint capacity); jint PushLocalFrame(JNIEnv *env, jint capacity); jobject PopLocalFrame(JNIEnv *env, jobject result);
jobject NewGlobalRef(JNIEnv *env, jobject obj);
|
|
Now we could save the result of NewGlobalRef(JNIEnv *, jobect) to a global/static variable and use it later, in another funciton call. |
void DeleteGlobalRef(JNIEnv *env, jobject globalRef);
jweak NewWeakGlobalRef(JNIEnv *env, jobject obj); void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);
/* On Unicode (UTF-16) Characters */ jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize len); jsize GetStringLength(JNIEnv *env, jstring string); const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy); void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars); void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize len, jchar *buf); /* On (modified) UTF-8 Characters */ jstring NewStringUTF(JNIEnv *env, const char *bytes); jsize GetStringUTFLength(JNIEnv *env, jstring string); const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy); void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf); void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize len, char *buf);
|
|
A note about GetString[UTF]Chars(…) functions
The pointer resulting from GetString[UTF]Chars(…) is valid until ReleaseString[UTF]Chars(…) is called. If isCopy is not NULL, then *isCopy is set to JNI_TRUE if a copy is made. When *isCopy == JNI_FALSE, the returned string is a direct pointer to the characters in the original java.lang.String instance, which is then pinned in memory. The native code must ensure not to modify the contents of the returned string, otherwise, it would be modifying the private data of the immutable java.lang.String object! Regardless of isCopy, we have to call ReleaseString[UTF]Chars(…) when we are done using the character array, either to free the memory (when *isCopy == JNI_TRUE) or to un-pin the original string in memory (when *isCopy == JNI_FALSE). |
|
|
A note about modified UTF-8 strings:
JNI’s UTF string functions (that work with char *) return/assume \0-terminated character arrays that are encoded as UTF-8 character sequences, except that if the string contains a \u0000 character, it is represented by a pair of bytes 0xc0 0x80 (1100000010000000) instead of 0x00. When working with regular ASCII characters, each character is represented by exactly one byte. |
#include <stdio.h> #include "com_marakana_jniexamples_HelloName.h" JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass clazz, jstring name) { printf("Hello %s", name); /**/ }
| This example would not work (would likely crash the VM) since the jstring type represents strings in the Java virtual machine. This is different from the C string type (char *). |
#include <stdio.h> #include "com_marakana_jniexamples_HelloName.h" JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass clazz, jstring name){ const char *cName = (*env)->GetStringUTFChars(env, name, NULL); /**/ if (cName == NULL) { return; /* OutOfMemoryError already thrown */ } else { printf("Hello %s\n", cName); (*env)->ReleaseStringUTFChars(env, name, cName); /*
*/ } }
| This returns a pointer to an array of bytes representing the string in modified UTF-8 encoding; or NULL if we ran out of memory, in which case java.lang.OutOfMemoryError would also be thrown (upon returning back to the Java runtime). | |
| Calling ReleaseStringUTFChars indicates that the native method no longer needs the UTF-8 string returned by GetStringUTFChars, thus the memory taken by the UTF-8 string can be freed. Failure to do this would result in a memory leak, which could ultimately lead to memory exhaustion. |
#include <stdio.h> #include "com_marakana_jniexamples_GetName.h" JNIEXPORT jstring JNICALL Java_com_marakana_jniexamples_ReturnName_GetName(JNIEnv *env, jclass class) { char buf[20]; fgets(buf, sizeof(buf), stdin); return (*env)->NewStringUTF(env, buf); }
#include <stdio.h> #include "com_marakana_jniexamples_HelloName.h" JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass clazz, jstring name) { char buf[4]; jint i; jsize len = (*env)->GetStringUTFLength(env, name); fputs("Hello ", stdout); for (i = 0; i < len; i++) { (*env)->GetStringUTFRegion(env, name, i, 1, buf); putc(buf[0], stdout); /* assumes ASCII */ } putc('\n', stdout); }
jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement); jbooleanArray NewBooleanArray(JNIEnv *env, jsize length); jbyteArray NewByteArray(JNIEnv *env, jsize length); jcharArray NewCharArray(JNIEnv *env, jsize length); jshortArray NewShortArray(JNIEnv *env, jsize length); jintArray NewIntArray(JNIEnv *env, jsize length); jlongArray NewLongArray(JNIEnv *env, jsize length); jfloatArray NewFloatArray(JNIEnv *env, jsize length); jdoubleArray NewDoubleArray(JNIEnv *env, jsize length);
jsize GetArrayLength(JNIEnv *env, jarray array);
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index); void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
jboolean* GetBooleanArrayElements(JNIEnv *env, jbooleanArray array, jboolean *isCopy); jbyte* GetByteArrayElements(JNIEnv *env, jbyteArray array, jboolean *isCopy); jchar* GetCharArrayElements(JNIEnv *env, jcharArray array, jboolean *isCopy); jshort* GetShortArrayElements(JNIEnv *env, jshortArray array, jboolean *isCopy); jint* GetIntArrayElements(JNIEnv *env, jintArray array, jboolean *isCopy); jlong* GetLongArrayElements(JNIEnv *env, jlongArray array, jboolean *isCopy); jfloat* GetFloatArrayElements(JNIEnv *env, jfloatArray array, jboolean *isCopy); jdouble* GetDoubleArrayElements(JNIEnv *env, jdoubleArray array, jboolean *isCopy); void ReleaseBooleanArrayElements(JNIEnv *env, jbooleanArray array, jboolean *elems, jint mode); void ReleaseByteArrayElements(JNIEnv *env, jbyteArray array, jbyte *elems, jint mode); void ReleaseCharArrayElements(JNIEnv *env, jcharArray array, jchar *elems, jint mode); void ReleaseShortArrayElements(JNIEnv *env, jshortArray array, jshort *elems, jint mode); void ReleaseIntArrayElements(JNIEnv *env, jintArray array, jint *elems, jint mode); void ReleaseLongArrayElements(JNIEnv *env, jlongArray array, jlong *elems, jint mode); void ReleaseFloatArrayElements(JNIEnv *env, jfloatArray array, jfloat *elems, jint mode); void ReleaseDoubleArrayElements(JNIEnv *env, jdoubleArray array, jdouble *elems, jint mode);
void GetBooleanArrayRegion(JNIEnv *env, jbooleanArray array, jsize start, jsize len, jboolean *buf); void GetByteArrayRegion(JNIEnv *env, jbyteArray array, jsize start, jsize len, jbyte *buf); void GetCharArrayRegion(JNIEnv *env, jcharArray array, jsize start, jsize len, jchar *buf); void GetShortArrayRegion(JNIEnv *env, jshortArray array, jsize start, jsize len, jshort *buf); void GetIntArrayRegion(JNIEnv *env, jintArray array, jsize start, jsize len, jint *buf); void GetLongArrayRegion(JNIEnv *env, jlongArray array, jsize start, jsize len, jlong *buf); void GetFloatArrayRegion(JNIEnv *env, jfloatArray array, jsize start, jsize len, jfloat *buf); void GetDoubleArrayRegion(JNIEnv *env, jdoubleArray array, jsize start, jsize len, jdouble *buf); void SetBooleanArrayRegion(JNIEnv *env, jbooleanArray array, jsize start, jsize len, jboolean *buf); void SetByteArrayRegion(JNIEnv *env, jbyteArray array, jsize start, jsize len, jbyte *buf); void SetCharArrayRegion(JNIEnv *env, jcharArray array, jsize start, jsize len, jchar *buf); void SetShortArrayRegion(JNIEnv *env, jshortArray array, jsize start, jsize len, jshort *buf); void SetIntArrayRegion(JNIEnv *env, jintArray array, jsize start, jsize len, jint *buf); void SetLongArrayRegion(JNIEnv *env, jlongArray array, jsize start, jsize len, jlong *buf); void SetFloatArrayRegion(JNIEnv *env, jfloatArray array, jsize start, jsize len, jfloat *buf); void SetDoubleArrayRegion(JNIEnv *env, jdoubleArray array, jsize start, jsize len, jdouble *buf);
JNIEXPORT jint JNICALL Java_com_marakana_jniexamples_Math_sum(JNIEnv *env, jclass clazz, jintArray array) { jint *cArray = (*env)->GetIntArrayElements(env, array, NULL); if (cArray == NULL) { return 0; } else { jini len = (*env)->GetArrayLength(env, array); jint i; jint result = 0; for (i = 0; i < len; i++) { result += cArray[i]; } (*env)->ReleaseIntArrayElements(env, array, cArray, JNI_ABORT); return result; } }
JNIEXPORT jint JNICALL Java_com_marakana_jniexamples_Math_sum(JNIEnv *env, jclass clazz, jintArray array) { jint buf[1]; jini len = (*env)->GetArrayLength(env, array); jint i; jint result = 0; for (i = 0; i < len; i++) { (*env)->GetIntArrayRegion(env, array, i, 1, buf); result += buf[0]; } return result; }
JNIEXPORT jintArray JNICALL Java_com_marakana_jniexamples_Foo_getData(JNIEnv *env, jclass class) { jint cArray[10]; jsize len = sizeof(cArray); jintArray jArray = (*env)->NewIntArray(env, len); if (jArray != NULL) { jint i; /* "get" the data */ for (i = 0; i < len; i++) { cArray[i] = i; } (*env)->SetIntArrayRegion(env, jArray, 0, len, cArray); } return jArray; }
jobject NewDirectByteBuffer(JNIEnv* env, void* address, jlong capacity); void* GetDirectBufferAddress(JNIEnv* env, jobject buf); jlong GetDirectBufferCapacity(JNIEnv* env, jobject buf);
public class Foo { public static void main(String[] args) { … ByteBuffer buf = ByteBuffer.allocateDirect(1024); // populate buf processData(buf); … } public native static void processData(ByteBuffer buf); }
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Foo_processData(JNIEnv *env, jclass clazz, jobject buf) { char *cBuf = (*env)->GetDirectBufferAddress(env, buf); /* process cBuf from 0 to (*env)->GetDirectBufferCapacity(env, buf) */ }
|
|
A note about memory
The pointer resulting from GetTypeArrayElements(…) is valid until ReleaseTypeArrayElements(…) is called (unless mode == JNI_COMMIT). If isCopy is not NULL, then *isCopy is set to JNI_TRUE if a copy is made. When *isCopy == JNI_FALSE, the returned array is a direct pointer to the elements of the Java array, which is then pinned in memory. Regardless of isCopy, we have to call ReleaseTypeArrayElements(…, int mode) when we are done using the native array, either to un-pin the Java array in memory when *isCopy == JNI_FALSE, or, when *isCopy == JNI_TRUE, to:
JNI also supports a critical version of these functions: void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy); void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode); The function GetPrimitiveArrayCritical(…) is similar to GetTypeArrayElements(…), except that the VM is more likely to return the pointer to the primitive array (i.e. *isCopy == JNI_FALSE is more likely). However, this comes with some caveats. The native code between GetPrimitiveArrayCritical(…) and ReleasePrimitiveArrayCritical(…) must not call other JNI functions, or make any system calls that may cause the current thread to block and wait for another Java thread. This is because the VM may temporarily suspend the garbage collection - in case it does not support memory pinning. |
jclass FindClass(JNIEnv *env, const char *name);
/* load the java.lang.String class */ (*env)->FindClass(env, "java/lang/String");
/* load the java.lang.String[] class */ (*env)->FindClass(env, "[Ljava/lang/String;");
jclass GetSuperclass(JNIEnv *env, jclass clazz); jboolean IsAssignableFrom(JNIEnv *env, jclass clazz1, jclass clazz2);
jclass GetObjectClass(JNIEnv *env, jobject obj); jboolean IsInstanceOf(JNIEnv *env, jobject obj, jclass clazz);
| Type Signature | Java Type |
|---|---|
Z |
boolean |
B |
byte |
C |
char |
S |
short |
I |
int |
J |
long |
F |
float |
D |
double |
Lfully-qualified-class; |
fully-qualified-class |
[type |
type[] |
(arg-types)ret-type |
method type |
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig); jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jobject (*GetStaticObjectField)(JNIEnv*, jclass, jfieldID); jboolean (*GetStaticBooleanField)(JNIEnv*, jclass, jfieldID); jbyte (*GetStaticByteField)(JNIEnv*, jclass, jfieldID); jchar (*GetStaticCharField)(JNIEnv*, jclass, jfieldID); jshort (*GetStaticShortField)(JNIEnv*, jclass, jfieldID); jint (*GetStaticIntField)(JNIEnv*, jclass, jfieldID); jlong (*GetStaticLongField)(JNIEnv*, jclass, jfieldID); jfloat (*GetStaticFloatField)(JNIEnv*, jclass, jfieldID); jdouble (*GetStaticDoubleField)(JNIEnv*, jclass, jfieldID); void (*SetStaticObjectField)(JNIEnv*, jclass, jfieldID, jobject); void (*SetStaticBooleanField)(JNIEnv*, jclass, jfieldID, jboolean); void (*SetStaticByteField)(JNIEnv*, jclass, jfieldID, jbyte); void (*SetStaticCharField)(JNIEnv*, jclass, jfieldID, jchar); void (*SetStaticShortField)(JNIEnv*, jclass, jfieldID, jshort); void (*SetStaticIntField)(JNIEnv*, jclass, jfieldID, jint); void (*SetStaticLongField)(JNIEnv*, jclass, jfieldID, jlong); void (*SetStaticFloatField)(JNIEnv*, jclass, jfieldID, jfloat); void (*SetStaticDoubleField)(JNIEnv*, jclass, jfieldID, jdouble); jobject (*GetObjectField)(JNIEnv*, jobject, jfieldID); jboolean (*GetBooleanField)(JNIEnv*, jobject, jfieldID); jbyte (*GetByteField)(JNIEnv*, jobject, jfieldID); jchar (*GetCharField)(JNIEnv*, jobject, jfieldID); jshort (*GetShortField)(JNIEnv*, jobject, jfieldID); jint (*GetIntField)(JNIEnv*, jobject, jfieldID); jlong (*GetLongField)(JNIEnv*, jobject, jfieldID); jfloat (*GetFloatField)(JNIEnv*, jobject, jfieldID); jdouble (*GetDoubleField)(JNIEnv*, jobject, jfieldID); void (*SetObjectField)(JNIEnv*, jobject, jfieldID, jobject); void (*SetBooleanField)(JNIEnv*, jobject, jfieldID, jboolean); void (*SetByteField)(JNIEnv*, jobject, jfieldID, jbyte); void (*SetCharField)(JNIEnv*, jobject, jfieldID, jchar); void (*SetShortField)(JNIEnv*, jobject, jfieldID, jshort); void (*SetIntField)(JNIEnv*, jobject, jfieldID, jint); void (*SetLongField)(JNIEnv*, jobject, jfieldID, jlong); void (*SetFloatField)(JNIEnv*, jobject, jfieldID, jfloat); void (*SetDoubleField)(JNIEnv*, jobject, jfieldID, jdouble);
public class Foo { private String bar; … public native void processBar(); }
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Foo_processBar(JNIEnv *env, jobject object) { /* Same as object.getClass() */ jclass clazz = (*env)->GetObjectClass(env, object); if (clazz != NULL) { /* cannot be null in this case */ /* Same as clazz.getField("bar") */ jfieldID field = (*env)->GetFieldID(env, clazz, "bar", "Ljava/lang/String;"); if (field != NULL) { /* make sure we got the field */ /* Same as field.get(object) */ jstring jString = (*env)->GetObjectField(env, object, field); if (jString != NULL) { /* Convert the value to a C (UTF-8) string */ const char *cString = (*env)->GetStringUTFChars(env, jString, NULL); if (cString == NULL) { return; /* Out of memory */ } printf("Value of \"bar\" before the change: \"%s\"\n", cString); (*env)->ReleaseStringUTFChars(env, jString, cString); } /* Create a new String */ jString = (*env)->NewStringUTF(env, "Bar2"); if (jString != NULL) { /* make sure we are not out of memory */ /* Same as field.set(object, jString) */ (*env)->SetObjectField(env, object, field, jString); } } } }
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig); jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jobject (*CallStaticObjectMethod)(JNIEnv*, jclass, jmethodID, ...); jboolean (*CallStaticBooleanMethod)(JNIEnv*, jclass, jmethodID, ...); jbyte (*CallStaticByteMethod)(JNIEnv*, jclass, jmethodID, ...); jchar (*CallStaticCharMethod)(JNIEnv*, jclass, jmethodID, ...); jshort (*CallStaticShortMethod)(JNIEnv*, jclass, jmethodID, ...); jint (*CallStaticIntMethod)(JNIEnv*, jclass, jmethodID, ...); jlong (*CallStaticLongMethod)(JNIEnv*, jclass, jmethodID, ...); jfloat (*CallStaticFloatMethod)(JNIEnv*, jclass, jmethodID, ...); jdouble (*CallStaticDoubleMethod)(JNIEnv*, jclass, jmethodID, ...); void (*CallStaticVoidMethod)(JNIEnv*, jclass, jmethodID, ...); jobject (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...); jboolean (*CallBooleanMethod)(JNIEnv*, jobject, jmethodID, ...); jbyte (*CallByteMethod)(JNIEnv*, jobject, jmethodID, ...); jchar (*CallCharMethod)(JNIEnv*, jobject, jmethodID, ...); jshort (*CallShortMethod)(JNIEnv*, jobject, jmethodID, ...); jint (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...); jlong (*CallLongMethod)(JNIEnv*, jobject, jmethodID, ...); jfloat (*CallFloatMethod)(JNIEnv*, jobject, jmethodID, ...); jdouble (*CallDoubleMethod)(JNIEnv*, jobject, jmethodID, ...); void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
|
|
JNI also offers Call…(…) functions that take args in a form of va_list as well as an array of jvalue-s: <Type> (*Call<Type>MethodV)(JNIEnv*, jobject, jmethodID, va_list); <Type> (*CallStatic<Type>MethodV)(JNIEnv*, jobject, jmethodID, va_list); <Type> (*Call<Type>MethodA)(JNIEnv*, jobject, jmethodID, jvalue*); <Type> (*CallStatic<Type>MethodA)(JNIEnv*, jobject, jmethodID, jvalue*); |
public class Foo { private String bar; public void setBar(String bar) { this.bar = bar; } … public native void processBar(); }
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Foo_processBar(JNIEnv *env, jobject object) { /* Same as object.getClass() */ jclass clazz = (*env)->GetObjectClass(env, object); if (clazz != NULL) { /* Same as clazz.getMethod("setBar", String.class) - assuming non-static */ jmethodID method = (*env)->GetMethodID(env, clazz, "setBar", "(Ljava/lang/String;)V"); if (method != NULL) { /* make sure we found the method */ /* Create a new Java String */ jstring jString = (*env)->NewStringUTF(env, "Bar2"); if (jString != null) { /* Same as method.invoke(object, jString) */ (*env)->CallVoidMethod(env, object, method, jString); } } } }
jint JNI_OnLoad(JavaVM *vm, void *reserved);
#include <jni.h> static void sayHi(JNIEnv *env, jclass clazz, jstring who, jint times) { const char *name = (*env)->GetStringUTFChars(env, who, NULL); if (name != NULL) { jint i; for (i = 0; i < times; i++) { printf("Hello %s\n", name); } (*env)->ReleaseStringUTFChars(env, who, name); } } static JNINativeMethod method_table[] = { { "sayHi", "(Ljava/lang/String;I)V", (void *) sayHi } }; static int method_table_size = sizeof(method_table) / sizeof(method_table[0]); jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env; if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } else { jclass clazz = (*env)->FindClass(env, "com/marakana/jniexamples/Hello"); if (clazz) { jint ret = (*env)->RegisterNatives(env, clazz, method_table, method_table_size); (*env)->DeleteLocalRef(env, clazz); return ret == 0 ? JNI_VERSION_1_6 : JNI_ERR; } else { return JNI_ERR; } } }
|
|
Notice that we JNI function that will be bound to the native Java method is declared as static. It won’t even be exported to the symbol table. It does not need to be because RegisterNatives will bind it via a function pointer to the Java method. |
jint Throw(JNIEnv *env, jthrowable obj);
jint ThrowNew(JNIEnv *env, jclass clazz, const char *message);
static void ThrowExceptionByClassName(JNIEnv *env, const char *name, const char *message) { jclass clazz = (*env)->FindClass(env, name); if (clazz != NULL) { (*env)->ThrowNew(env, clazz, message); (*env)->DeleteLocalRef(env, clazz); } } … if (invalidArgument == TRUE) { ThrowExceptionByClassName(env, "java/lang/IllegalArgumentException", "This argument is not valid!"); }
jthrowable ExceptionOccurred(JNIEnv *env); /* NULL if no exception is currently being thrown */ jboolean ExceptionCheck(JNIEnv *env); void ExceptionDescribe(JNIEnv *env); void ExceptionClear(JNIEnv *env);
… (*env)->CallObjectMethod(env, …); /* this can throw an exception */ if ((*env)->ExceptionCheck(env)) { jthrowable throwable = (*env)->ExceptionOccurred(env); (*env)->ExceptionDescribe(env); /* optionally dump the stack trace */ (*env)->ExceptionClear(env); /* mark the exception as "handled" */ jclazz clazz = (*env)->GetObjectClass(env, throwable); jmethodID getMessageMethod = (*env)->GetMethodID(env, clazz, "getMessage", "()Ljava/lang/String;"); jstring message = (*env)->CallObjectMethod(env, throwable, getMessageMethod); const char *cMessage = (*env)->GetStringUTFChars(env, message, NULL); if (cMessage) { printf("ERROR: %s\n", cMessage); (*env)->ReleaseStringUTFChars(env, message, cMessage); } (*env)->DeleteLocalRef(env, clazz); } …
We start off by defining C function prototypes as native Java methods (wrapped in some class):
package com.marakana.android.fibonaccinative; import android.util.Log; public class FibLib { private static final String TAG = "FibLib"; private static long fib(long n) { return n <= 0 ? 0 : n == 1 ? 1 : fib(n - 1) + fib(n - 2); } // Recursive Java implementation of the Fibonacci algorithm // (included for comparison only) public static long fibJR(long n) { Log.d(TAG, "fibJR(" + n + ")"); return fib(n); } // Function prototype for future native recursive implementation // of the Fibonacci algorithm public native static long fibNR(long n); // Iterative Java implementation of the Fibonacci algorithm // (included for comparison only) public static long fibJI(long n) { Log.d(TAG, "fibJI(" + n + ")"); long previous = -1; long result = 1; for (long i = 0; i <= n; i++) { long sum = result + previous; previous = result; result = sum; } return result; } // Function prototype for future iterative recursive implementation // of the Fibonacci algorithm public native static long fibNI(long n); static { // as defined by LOCAL_MODULE in Android.mk System.loadLibrary("com_marakana_android_fibonaccinative_FibLib"); } }
We then extract our C header file with our function prototypes:
On the command line, change to your project’s root directory
$ cd /path/to/workspace/FibonacciNative
Create jni sub-directory
$ mkdir jni
Extract the C header file from com.marakana.android.fibonaccinative.FibLib class:
$ javah -jni -classpath bin/classes -d jni com.marakana.android.fibonaccinative.FibLib
|
|
Prior to ADT r14, compiled class files were kept directly in the bin/ directory, so in our javah command we would’ve used -classpath bin instead. |
Check out the resulting file:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_marakana_android_fibonaccinative_FibLib */ #ifndef _Included_com_marakana_android_fibonaccinative_FibLib #define _Included_com_marakana_android_fibonaccinative_FibLib #ifdef __cplusplus extern "C" { #endif /* * Class: com_marakana_android_fibonaccinative_FibLib * Method: fibNR * Signature: (J)J */ JNIEXPORT jlong JNICALL Java_com_marakana_android_fibonaccinative_FibLib_fibNR (JNIEnv *, jclass, jlong); /* * Class: com_marakana_android_fibonaccinative_FibLib * Method: fibNI * Signature: (J)J */ JNIEXPORT jlong JNICALL Java_com_marakana_android_fibonaccinative_FibLib_fibNI (JNIEnv *, jclass, jlong); #ifdef __cplusplus } #endif #endif
|
|
The function prototype names are name-spaced to the classname they are found in. |
We provide the C implementation of com_marakana_android_fibonacci_FibLib.h header file:
/* Include the header file that was created via "javah -jni" command */ #include "com_marakana_android_fibonaccinative_FibLib.h" #include <android/log.h> /* Recursive implementation of the fibonacci algorithm (in a helper function) */ static jlong fib(jlong n) { return n <= 0 ? 0 : n == 1 ? 1 : fib(n - 1) + fib(n - 2); } /* Actual implementation of JNI-defined `fibNR` (recursive) function */ JNIEXPORT jlong JNICALL Java_com_marakana_android_fibonaccinative_FibLib_fibNR (JNIEnv *env, jclass clazz, jlong n) { __android_log_print(ANDROID_LOG_DEBUG, "FibLib.c", "fibNR(%lld)", n); return fib(n); } /* Actual implementation of JNI-defined `fibNI` (iterative) function */ JNIEXPORT jlong JNICALL Java_com_marakana_android_fibonaccinative_FibLib_fibNI (JNIEnv *env, jclass clazz, jlong n) { jlong previous = -1; jlong result = 1; jlong i; __android_log_print(ANDROID_LOG_DEBUG, "FibLib.c", "fibNI(%lld)", n); for (i = 0; i <= n; i++) { jlong sum = result + previous; previous = result; result = sum; } return result; }
We could also use an alternative mechanism of linking native-code to managed code by pre-registering our functions. This leads to earlier detection of method-function mismatch issues, a slight performance improvement, and spares us the redundancy of the header file and the use of the javah command.
#include <jni.h> #include <android/log.h> namespace com_marakana_android_fibonaccinative { static jlong fib(jlong n) { return n <= 0 ? 0 : n == 1 ? 1 : fib(n - 1) + fib(n - 2); } static jlong fibNR(JNIEnv *env, jclass clazz, jlong n) { __android_log_print(ANDROID_LOG_DEBUG, "FibLib.c", "fibNR(%lld)", n); return fib(n); } static jlong fibNI(JNIEnv *env, jclass clazz, jlong n) { jlong previous = -1; jlong result = 1; jlong i; __android_log_print(ANDROID_LOG_DEBUG, "FibLib.c", "fibNI(%lld)", n); for (i = 0; i <= n; i++) { jlong sum = result + previous; previous = result; result = sum; } return result; } static JNINativeMethod method_table[] = { { "fibNR", "(J)J", (void *) fibNR }, { "fibNI", "(J)J", (void *) fibNI } }; } using namespace com_marakana_android_fibonaccinative; extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env; if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } else { jclass clazz = env->FindClass("com/marakana/android/fibonaccinative/FibLib"); if (clazz) { jint ret = env->RegisterNatives(clazz, method_table, sizeof(method_table) / sizeof(method_table[0])); env->DeleteLocalRef(clazz); return ret == 0 ? JNI_VERSION_1_6 : JNI_ERR; } else { return JNI_ERR; } } }
|
|
Most of the Android’s JNI-based shared libraries are built using this, "alternative", approach where the functions are pre-registered. |
We need a Android.mk makefile, which will be used by NDK to compile our JNI code into a shared library:
# Defines the root to all other relative paths # The macro function my-dir, provided by the build system, # specifies the path of the current directory (i.e. the # directory containing the Android.mk file itself) LOCAL_PATH := $(call my-dir) # Clear all LOCAL_XXX variables with the exception of # LOCAL_PATH (this is needed because all variables are global) include $(CLEAR_VARS) # List all of our C files to be compiled (header file # dependencies are automatically computed) LOCAL_SRC_FILES := com_marakana_android_fibonaccinative_FibLib.c # The name of our shared module (this name will be prepended # by lib and postfixed by .so) LOCAL_MODULE := com_marakana_android_fibonaccinative_FibLib # We need to tell the linker about our use of the liblog.so LOCAL_LDLIBS += -llog # Collects all LOCAL_XXX variables since "include $(CLEAR_VARS)" # and determines what to build (in this case a shared library) include $(BUILD_SHARED_LIBRARY)
|
|
It’s easiest to copy the Android.mk file from another (sample) project and adjust LOCAL_SRC_FILES and LOCAL_MODULE as necessary |
|
|
See /path/to/ndk-installation-dir/docs/ANDROID-MK.html for the complete reference of Android make files (build system) |
Finally, from the root of our project (i.e. FibonacciNative/), we run ndk-build to build our code into a shared library (FibonacciNative/libs/armeabi/libcom_marakana_android_fibonacci_FibLib.so):
$ ndk-build Compile thumb : com_marakana_android_fibonaccinative_FibLib <= com_marakana_android_fibonaccinative_FibLib.c SharedLibrary : libcom_marakana_android_fibonaccinative_FibLib.so Install : libcom_marakana_android_fibonaccinative_FibLib.so => libs/armeabi/libcom_marakana_android_fibonaccinative_FibLib.so
|
|
The command ndk-build comes from the NDK’s installation directory (e.g. /path/to/android-ndk-r5b), so it’s easiest if we add this directory to our PATH. |
|
|
On Windows, older version of NDK required Cygwin (a Unix-like environment and command-line interface for Microsoft Windows) to provide "shell" (bash) and "make" (gmake) to ndk-build. |
By default, the NDK will generate machine code for the armeabi - i.e. ARMv5TE with support for Thumb-1.
In addition to ARMv5, NDK also comes with the toolchains necessary to build code for:
We can explicitly select ABIs using APP_ABI variable:
$ ndk-build APP_ABI=armeabi $ ndk-build APP_ABI=armeabi-v7a $ ndk-build APP_ABI=x86 $ ndk-build APP_ABI=mips
We can also combine them, thereby building a "fat binary":
$ ndk-build "APP_ABI=armeabi armeabi-v7a x86 mips" $ ndk-build APP_ABI=all
Finally, we can also persist our choice of ABI, by saving APP_ABI in a Application.mk file:
APP_ABI := allEach ABI-specific library, gets packaged as lib/<ABI>/lib<name>.so inside our APK.
Upon installation, the library that best matches the native ABI of the device it will execute on, will get copied to /data/data/<package>/lib/lib<name>.so.
To remove all generated binaries, run:
$ ndk-build clean Clean: com_marakana_android_fibonaccinative_FibLib [armeabi] Clean: stdc++ [armeabi]
We can now build the "client" of our library (in this case a simple activity) to use our FibLib library.
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hello">Get Your Fibonacci Numbers Here!</string> <string name="fibJR">fibJR</string> <string name="fibJI">fibJI</string> <string name="fibNR">fibNR</string> <string name="fibNI">fibNI</string> <string name="app_name">FibonacciNative</string> <string name="button">Get Fibonacci Result</string> </resources>
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <!-- This is just a simple title ("Get Your Fibonacci Here!") --> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center" android:text="@string/hello" android:textSize="25sp" /> <!-- This is the entry box for our number "n" --> <EditText android:id="@+id/input" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:gravity="right" android:inputType="number" > <requestFocus /> </EditText> <!-- This radio group allows the user to select the fibonacci implementation type --> <RadioGroup android:id="@+id/type" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <RadioButton android:id="@+id/type_fib_jr" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/fibJR" /> <RadioButton android:id="@+id/type_fib_ji" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/fibJI" /> <RadioButton android:id="@+id/type_fib_nr" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/fibNR" /> <RadioButton android:id="@+id/type_fib_ni" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/fibNI" /> </RadioGroup> <!-- This button allows the user to trigger fibonacci calculation --> <Button android:id="@+id/button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/button" /> <!-- This is the output area for the fibonacci result --> <TextView android:id="@+id/output" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:textSize="20sp" /> </LinearLayout>
package com.marakana.android.fibonaccinative; import android.app.Activity; import android.app.ProgressDialog; import android.os.AsyncTask; import android.os.Bundle; import android.os.SystemClock; import android.text.TextUtils; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.RadioGroup; import android.widget.TextView; public class FibonacciActivity extends Activity implements OnClickListener { private EditText input; private RadioGroup type; private TextView output; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); this.input = (EditText) super.findViewById(R.id.input); this.type = (RadioGroup) super.findViewById(R.id.type); this.output = (TextView) super.findViewById(R.id.output); Button button = (Button) super.findViewById(R.id.button); button.setOnClickListener(this); } public void onClick(View view) { String s = this.input.getText().toString(); if (TextUtils.isEmpty(s)) { return; } final ProgressDialog dialog = ProgressDialog.show(this, "", "Calculating...", true); final long n = Long.parseLong(s); new AsyncTask<Void, Void, String>() { @Override protected String doInBackground(Void... params) { long result = 0; long t = SystemClock.uptimeMillis(); switch (FibonacciActivity.this.type.getCheckedRadioButtonId()) { case R.id.type_fib_jr: result = FibLib.fibJR(n); break; case R.id.type_fib_ji: result = FibLib.fibJI(n); break; case R.id.type_fib_nr: result = FibLib.fibNR(n); break; case R.id.type_fib_ni: result = FibLib.fibNI(n); break; } t = SystemClock.uptimeMillis() - t; return String.format("fib(%d)=%d in %d ms", n, result, t); } @Override protected void onPostExecute(String result) { dialog.dismiss(); FibonacciActivity.this.output.setText(result); } }.execute(); } }
The header files for NDK stable APIs are available at /path/to/ndk/platforms/<android-platform>/<arch-name>/usr/include.
|
|
With the exception of the libraries listed above, the native system libraries in the Android platform are not considered "stable" and may change in future platform versions. Unless our library is being built for a specific Android ROM, we should only make use of the stable libraries provided by the NDK. |
|
|
All the header files are available under: /path/to/ndk-installation-dir/platforms/android-9/arch-arm/usr/include/ |
|
|
See /path/to/ndk-installation-dir/docs/STABLE-APIS.html for the complete reference of NDK’s stable APIs. |
The objective of this lab is to test your understanding of JNI and NDK. We will do so by adding JNI code to an existing application.
Start by importing LogNative application into Eclipse
Menu Bar → File → Import… → Git → Projects from Git → Next >
Under Select Repository Source select URI → Next >
Under Source Git Repository → Location → URI: enter https://github.com/marakana/LogNative.git → Next >
Under Branch Selection, leave all branches selected (checked) → Next >
Under Local Destination → Destination specify directory of your choice (e.g. ~/android/workspace/LogNative) → Next >
Under Select a wizard to use for importing projects, leave Wizard for project import as Import existing projects → Next >
Under Import Projects → Projects, leave LogNative as selected (checked) → Finish
|
|
This project can also be downloaded as a ZIP file |
Examine and test your project in Eclipse
Run the application on a device/emulator
Enter some tag and message to log, click on the Log button and observe via adb logcat that your message get logged (assuming Java was selected)
Implement com.marakana.android.lognative.LogLib.logN(int priority, String tag, String msg) in C
Mark the method as native
Remove its body
Extract its function prototype into a C header file (hint: javah)
Implement the function by taking advantage of <android/log.h> (i.e. /system/lib/liblog.so)
Provide the makefile(s)
Build (via ndk-build)
Run your application
Test by selecting Native in the UI and checking that the log tag/message shows up in adb logcat
As a bonus:
Throw java.lang.NullPointerException if tag or msg are null
Throw java.lang.IllegalArgumentException if priority is not one of the allowed types or if tag or msg are empty
|
|
Don’t forget to convert tag and msg strings from the Java format (jstring) to native format (char *) before trying to use them in int __android_log_write(int prio, const char *tag, const char *text). Be sure to free the native strings before returning from the native method. Finally, don’t forget to tell the linker about your use of the log library. |
The solution is provided:
Who uses JNI on Android and why?
What is the NDK?
What is JNI?
What is the purpose of jni.h?
What is the purpose of javah tool?
What are the first two arguments to all native functions that model Java methods declared as native?
What is jlong and why do we need it (and its cousins)?
How are boolean-s modeled in native code?
How are java.lang.Object-s modeled in native code?
What’s the definition of NULL?
What is the difference between local and global references?
What is the difference between Java strings and C "strings"?
What is the difference between GetStringChars and GetStringUTFChars?
How do modified UTF-8 strings differ from normal UTF-8 strings?
Why do we have to call ReleaseStringChars or ReleaseStringUTFChars when we are done with the C strings?
Name at least five array operations?
What is the purpose of mode attribute in ReleaseByteArrayElements(…, mode) (and its cousins)?
What is the signature of void f(int[] a, String s, long n)?
What happens when an exception is thrown by native code?
What is the significance of extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) function?
What can we do with jint RegisterNatives(JNIEnv*, jclass, const JNINativeMethod*, jint) function?
What does LOCAL_MODULE specify in Android.mk?
How do we control the ABI when compiling NDK code?
Name at least three stable NDK APIs.
In this module, you learned how Android uses JNI to bridge between the world of Java and the native code. You also learned how NDK makes the process of working with JNI simpler by providing tools and framework for developing native libraries as well as packaging them with the app.
package com.example.app; import com.example.app.Bar; interface IFooService { void save(inout Bar bar); Bar getById(int id); void delete(in Bar bar); List<Bar> getAll(); }
package com.example.app; public interface IFooService extends android.os.IInterface { public static abstract class Stub extends android.os.Binder implements com.example.app.IFooService { … public static com.example.app.IFooService asInterface( android.os.IBinder obj) { … } public android.os.IBinder asBinder() { return this; } … } void save(com.example.app.Bar bar) throws android.os.RemoteException; com.example.app.Bar getById(int id) throws android.os.RemoteException; void delete(com.example.app.Bar bar) throws android.os.RemoteException; java.util.List<Bar> getAll() throws android.os.RemoteException; }
|
|
Eclipse ADT automatically calls aidl for each .aidl file that it finds in our src/ directory |
package com.example.app; import android.os.Parcel; import android.os.Parcelable; public class Bar implements Parcelable { private int id; private String data; public Bar(int id, String data) { this.id = id; this.data = data; } // getters and setters omitted … public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(this.id); parcel.writeString(this.data); } public void readFromParcel(Parcel parcel) { this.id = parcel.readInt(); this.data = parcel.readString(); } public static final Parcelable.Creator<Bar> CREATOR = new Parcelable.Creator<Bar>() { public Bar createFromParcel(Parcel parcel) { return new Bar(parcel.readInt(), parcel.readString()); } public Bar[] newArray(int size) { return new Bar[size]; } }; }
|
|
Here, the public void readFromParcel(Parcel) method is not defined by the Parcelable interface. Instead, it would be required here because Bar is considered mutable - i.e. we expect the remote side to be able to change it in void save(inout Bar bar) method. Similarly, public static final Parcelable.Creator<Bar> CREATOR field is also not defined by the Parcelable interface (obviously). It would needed to reconstruct Bar in response to com.example.app.Bar getById(int id) operation. |
package com.example.app; parcelable Bar;
|
|
AIDL-interfaces have to import parcelable custom classes even if they are in the same package. In the case of the previous example, src/com/example/app/IFooService.aidl would have to import com.example.app.Bar; if it makes any references to com.example.app.Bar even though they are in the same package. |
… static int open_driver() { int fd = open("/dev/binder", O_RDWR); if (fd >= 0) { … size_t maxThreads = 15; result = ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads); … } else { … } return fd; } …
FibonacciCommon library project - to define our AIDL interface as well as custom types for parameters and return values
FibonacciService project - where we implement our AIDL interface and expose it to the clients
FibonacciClient project - where we connect to our AIDL-defined service and use it
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.marakana.android.fibonaccicommon" android:versionCode="1" android:versionName="1.0"> </manifest>
package com.marakana.android.fibonaccicommon; import com.marakana.android.fibonaccicommon.FibonacciRequest; import com.marakana.android.fibonaccicommon.FibonacciResponse; interface IFibonacciService { long fibJR(in long n); long fibJI(in long n); long fibNR(in long n); long fibNI(in long n); FibonacciResponse fib(in FibonacciRequest request); }
package com.marakana.android.fibonaccicommon; parcelable FibonacciRequest;
package com.marakana.android.fibonaccicommon; import android.os.Parcel; import android.os.Parcelable; public class FibonacciRequest implements Parcelable { public static enum Type { RECURSIVE_JAVA, ITERATIVE_JAVA, RECURSIVE_NATIVE, ITERATIVE_NATIVE } private final long n; private final Type type; public FibonacciRequest(long n, Type type) { this.n = n; if (type == null) { throw new NullPointerException("Type must not be null"); } this.type = type; } public long getN() { return n; } public Type getType() { return type; } public int describeContents() { return 0; } public void writeToParcel(Parcel parcel, int flags) { parcel.writeLong(this.n); parcel.writeInt(this.type.ordinal()); } public static final Parcelable.Creator<FibonacciRequest> CREATOR = new Parcelable.Creator<FibonacciRequest>() { public FibonacciRequest createFromParcel(Parcel in) { long n = in.readLong(); Type type = Type.values()[in.readInt()]; return new FibonacciRequest(n, type); } public FibonacciRequest[] newArray(int size) { return new FibonacciRequest[size]; } }; }
package com.marakana.android.fibonaccicommon; parcelable FibonacciResponse;
package com.marakana.android.fibonaccicommon; import android.os.Parcel; import android.os.Parcelable; public class FibonacciResponse implements Parcelable { private final long result; private final long timeInMillis; public FibonacciResponse(long result, long timeInMillis) { this.result = result; this.timeInMillis = timeInMillis; } public long getResult() { return result; } public long getTimeInMillis() { return timeInMillis; } public int describeContents() { return 0; } public void writeToParcel(Parcel parcel, int flags) { parcel.writeLong(this.result); parcel.writeLong(this.timeInMillis); } public static final Parcelable.Creator<FibonacciResponse> CREATOR = new Parcelable.Creator<FibonacciResponse>() { public FibonacciResponse createFromParcel(Parcel in) { return new FibonacciResponse(in.readLong(), in.readLong()); } public FibonacciResponse[] newArray(int size) { return new FibonacciResponse[size]; } }; }
package com.marakana.android.fibonaccicommon; public interface IFibonacciService extends android.os.IInterface { public static abstract class Stub extends android.os.Binder implements com.marakana.android.fibonacci.IFibonacciService { … public static com.marakana.android.fibonacci.IFibonacciService asInterface( android.os.IBinder obj) { … } public android.os.IBinder asBinder() { return this; } … } public long fibJR(long n) throws android.os.RemoteException; public long fibJI(long n) throws android.os.RemoteException; public long fibNR(long n) throws android.os.RemoteException; public long fibNI(long n) throws android.os.RemoteException; public com.marakana.android.fibonaccicommon.FibonacciResponse fib( com.marakana.android.fibonaccicommon.FibonacciRequest request) throws android.os.RemoteException; }
package com.marakana.android.fibonacciservice; import android.os.SystemClock; import android.util.Log; import com.marakana.android.fibonaccicommon.FibonacciRequest; import com.marakana.android.fibonaccicommon.FibonacciResponse; import com.marakana.android.fibonaccicommon.IFibonacciService; import com.marakana.android.fibonaccinative.FibLib; public class IFibonacciServiceImpl extends IFibonacciService.Stub { private static final String TAG = "IFibonacciServiceImpl"; public long fibJI(long n) { Log.d(TAG, String.format("fibJI(%d)", n)); return FibLib.fibJI(n); } public long fibJR(long n) { Log.d(TAG, String.format("fibJR(%d)", n)); return FibLib.fibJR(n); } public long fibNI(long n) { Log.d(TAG, String.format("fibNI(%d)", n)); return FibLib.fibNI(n); } public long fibNR(long n) { Log.d(TAG, String.format("fibNR(%d)", n)); return FibLib.fibNR(n); } public FibonacciResponse fib(FibonacciRequest request) { Log.d(TAG, String.format("fib(%d, %s)", request.getN(), request.getType())); long timeInMillis = SystemClock.uptimeMillis(); long result; switch (request.getType()) { case ITERATIVE_JAVA: result = this.fibJI(request.getN()); break; case RECURSIVE_JAVA: result = this.fibJR(request.getN()); break; case ITERATIVE_NATIVE: result = this.fibNI(request.getN()); break; case RECURSIVE_NATIVE: result = this.fibNR(request.getN()); break; default: return null; } timeInMillis = SystemClock.uptimeMillis() - timeInMillis; return new FibonacciResponse(result, timeInMillis); } }
package com.marakana.android.fibonacciservice; import android.app.Service; import android.content.Intent; import android.os.IBinder; import android.util.Log; public class FibonacciService extends Service { //private static final String TAG = "FibonacciService"; private IFibonacciServiceImpl service; //
@Override public void onCreate() { super.onCreate(); this.service = new IFibonacciServiceImpl(); //
Log.d(TAG, "onCreate()'ed"); //
} @Override public IBinder onBind(Intent intent) { Log.d(TAG, "onBind()'ed"); //
return this.service; //
} @Override public boolean onUnbind(Intent intent) { Log.d(TAG, "onUnbind()'ed"); //
return super.onUnbind(intent); } @Override public void onDestroy() { Log.d(TAG, "onDestroy()'ed"); this.service = null; super.onDestroy(); } }
| We create yet another "service" object by extending from android.app.Service. The purpose of FibonacciService object is to provide access to our Binder-based IFibonacciServiceImpl object. | |
| Here we simply declare a local reference to IFibonacciServiceImpl, which will act as a singleton (i.e. all clients will share a single instance). Since our IFibonacciServiceImpl does not require any special initialization, we could instantiate it at this point, but we choose to delay this until the onCreate() method. | |
| Now we instantiate our IFibonacciServiceImpl that we’ll be providing to our clients (in the onBind(Intent) method). If our IFibonacciServiceImpl required access to the Context (which it doesn’t) we could pass a reference to this (i.e. android.app.Service, which implements android.content.Context) at this point. Many Binder-based services use Context in order to access other platform functionality. | |
| This is where we provide access to our IFibonacciServiceImpl object to our clients. By design, we chose to have only one instance of IFibonacciServiceImpl (so all clients share it) but we could also provide each client with their own instance of IFibonacciServiceImpl. | |
| We just add some logging calls to make it easy to track the life-cycle of our service. |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.marakana.android.fibonacciservice" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="8" /> <application android:icon="@drawable/icon" android:label="@string/app_name"> <service android:name=".FibonacciService"> <intent-filter> <action android:name="com.marakana.android.fibonaccicommon.IFibonacciService" /> <!----> </intent-filter> </service> </application> </manifest>
| The name of this action is arbitrary, but it is a common convention to use the fully-qualified name of our AIDL-derived interface. |
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hello">Get Your Fibonacci Here!</string> <string name="app_name">Fibonacci Client</string> <string name="input_hint">Enter N</string> <string name="input_error">Numbers only!</string> <string name="button_text">Get Fib Result</string> <string name="progress_text">Calculating...</string> <string name="fib_error">Failed to get Fibonacci result</string> <string name="type_fib_jr">fibJR</string> <string name="type_fib_ji">fibJI</string> <string name="type_fib_nr">fibNR</string> <string name="type_fib_ni">fibNI</string> </resources>
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:text="@string/hello" android:layout_height="wrap_content" android:layout_width="fill_parent" android:textSize="25sp" android:gravity="center"/> <EditText android:layout_height="wrap_content" android:layout_width="match_parent" android:id="@+id/input" android:hint="@string/input_hint" android:inputType="number" android:gravity="right" /> <RadioGroup android:orientation="horizontal" android:layout_width="match_parent" android:id="@+id/type" android:layout_height="wrap_content"> <RadioButton android:layout_height="wrap_content" android:checked="true" android:id="@+id/type_fib_jr" android:text="@string/type_fib_jr" android:layout_width="match_parent" android:layout_weight="1" /> <RadioButton android:layout_height="wrap_content" android:id="@+id/type_fib_ji" android:text="@string/type_fib_ji" android:layout_width="match_parent" android:layout_weight="1" /> <RadioButton android:layout_height="wrap_content" android:id="@+id/type_fib_nr" android:text="@string/type_fib_nr" android:layout_width="match_parent" android:layout_weight="1" /> <RadioButton android:layout_height="wrap_content" android:id="@+id/type_fib_ni" android:text="@string/type_fib_ni" android:layout_width="match_parent" android:layout_weight="1" /> </RadioGroup> <Button android:text="@string/button_text" android:id="@+id/button" android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:id="@+id/output" android:layout_width="match_parent" android:layout_height="match_parent" android:textSize="20sp" android:gravity="center|top"/> </LinearLayout>
package com.marakana.android.fibonacciclient; import android.app.Activity; import android.app.ProgressDialog; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.RadioGroup; import android.widget.TextView; import android.widget.Toast; import com.marakana.android.fibonaccicommon.FibonacciRequest; import com.marakana.android.fibonaccicommon.FibonacciResponse; import com.marakana.android.fibonaccicommon.IFibonacciService; public class FibonacciActivity extends Activity implements OnClickListener, ServiceConnection { private static final String TAG = "FibonacciActivity"; private EditText input; // our input n private Button button; // trigger for fibonacci calcualtion private RadioGroup type; // fibonacci implementation type private TextView output; // destination for fibonacci result private IFibonacciService service; // reference to our service @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); super.setContentView(R.layout.main); // connect to our UI elements this.input = (EditText) super.findViewById(R.id.input); this.button = (Button) super.findViewById(R.id.button); this.type = (RadioGroup) super.findViewById(R.id.type); this.output = (TextView) super.findViewById(R.id.output); // request button click call-backs via onClick(View) method this.button.setOnClickListener(this); // the button will be enabled once we connect to the service this.button.setEnabled(false); } @Override protected void onResume() { Log.d(TAG, "onResume()'ed"); super.onResume(); // Bind to our FibonacciService service, by looking it up by its name // and passing ourselves as the ServiceConnection object // We'll get the actual IFibonacciService via a callback to // onServiceConnected() below if (!super.bindService(new Intent(IFibonacciService.class.getName()), this, BIND_AUTO_CREATE)) { Log.w(TAG, "Failed to bind to service"); } } @Override protected void onPause() { Log.d(TAG, "onPause()'ed"); super.onPause(); // No need to keep the service bound (and alive) any longer than // necessary super.unbindService(this); } public void onServiceConnected(ComponentName name, IBinder service) { Log.d(TAG, "onServiceConnected()'ed to " + name); // finally we can get to our IFibonacciService this.service = IFibonacciService.Stub.asInterface(service); // enable the button, because the IFibonacciService is initialized this.button.setEnabled(true); } public void onServiceDisconnected(ComponentName name) { Log.d(TAG, "onServiceDisconnected()'ed to " + name); // our IFibonacciService service is no longer connected this.service = null; // disabled the button, since we cannot use IFibonacciService this.button.setEnabled(false); } // handle button clicks public void onClick(View view) { // parse n from input (or report errors) final long n; String s = this.input.getText().toString(); if (TextUtils.isEmpty(s)) { return; } try { n = Long.parseLong(s); } catch (NumberFormatException e) { this.input.setError(super.getText(R.string.input_error)); return; } // build the request object final FibonacciRequest.Type type; switch (FibonacciActivity.this.type.getCheckedRadioButtonId()) { case R.id.type_fib_jr: type = FibonacciRequest.Type.RECURSIVE_JAVA; break; case R.id.type_fib_ji: type = FibonacciRequest.Type.ITERATIVE_JAVA; break; case R.id.type_fib_nr: type = FibonacciRequest.Type.RECURSIVE_NATIVE; break; case R.id.type_fib_ni: type = FibonacciRequest.Type.ITERATIVE_NATIVE; break; default: return; } final FibonacciRequest request = new FibonacciRequest(n, type); // showing the user that the calculation is in progress final ProgressDialog dialog = ProgressDialog.show(this, "", super.getText(R.string.progress_text), true); // since the calculation can take a long time, we do it in a separate // thread to avoid blocking the UI new AsyncTask<Void, Void, String>() { @Override protected String doInBackground(Void... params) { // this method runs in a background thread try { long totalTime = SystemClock.uptimeMillis(); FibonacciResponse response = FibonacciActivity.this.service .fib(request); totalTime = SystemClock.uptimeMillis() - totalTime; // generate the result return String.format( "fibonacci(%d)=%d\nin %d ms\n(+ %d ms)", n, response.getResult(), response.getTimeInMillis(), totalTime - response.getTimeInMillis()); } catch (RemoteException e) { Log.wtf(TAG, "Failed to communicate with the service", e); return null; } } @Override protected void onPostExecute(String result) { // get rid of the dialog dialog.dismiss(); if (result == null) { // handle error Toast.makeText(FibonacciActivity.this, R.string.fib_error, Toast.LENGTH_SHORT).show(); } else { // show the result to the user FibonacciActivity.this.output.setText(result); } } }.execute(); // run our AsyncTask } }
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.marakana.android.fibonacciclient"> <uses-sdk android:minSdkVersion="8" /> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name="com.marakana.android.fibonacciclient.FibonacciActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
package com.marakana.android.fibonaccicommon; import com.marakana.android.fibonaccicommon.FibonacciRequest; import com.marakana.android.fibonaccicommon.FibonacciResponse; oneway interface IFibonacciServiceResponseListener { void onResponse(in FibonacciResponse response); }
package com.marakana.android.fibonaccicommon; import com.marakana.android.fibonaccicommon.FibonacciRequest; import com.marakana.android.fibonaccicommon.FibonacciResponse; import com.marakana.android.fibonaccicommon.IFibonacciServiceResponseListener; oneway interface IFibonacciService { void fib(in FibonacciRequest request, in IFibonacciServiceResponseListener listener); }
package com.marakana.android.fibonacciservice; import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; import com.marakana.android.fibonaccicommon.FibonacciRequest; import com.marakana.android.fibonaccicommon.FibonacciResponse; import com.marakana.android.fibonaccicommon.IFibonacciService; import com.marakana.android.fibonaccicommon.IFibonacciServiceResponseListener; import com.marakana.android.fibonaccinative.FibLib; public class IFibonacciServiceImpl extends IFibonacciService.Stub { private static final String TAG = "IFibonacciServiceImpl"; @Override public void fib(FibonacciRequest request, IFibonacciServiceResponseListener listener) throws RemoteException { long n = request.getN(); Log.d(TAG, "fib(" + n + ")"); long timeInMillis = SystemClock.uptimeMillis(); long result; switch (request.getType()) { case ITERATIVE_JAVA: result = FibLib.fibJI(n); break; case RECURSIVE_JAVA: result = FibLib.fibJR(n); break; case ITERATIVE_NATIVE: result = FibLib.fibNI(n); break; case RECURSIVE_NATIVE: result = FibLib.fibNR(n); break; default: result = 0; } timeInMillis = SystemClock.uptimeMillis() - timeInMillis; Log.d(TAG, String.format("Got fib(%d) = %d in %d ms", n, result, timeInMillis)); listener.onResponse(new FibonacciResponse(result, timeInMillis)); } }
|
|
The service will not block waiting for the listener to return, because the listener itself is also oneway. |
package com.marakana.android.fibonacciclient; import android.app.Activity; import android.app.Dialog; import android.app.ProgressDialog; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.RemoteException; import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.RadioGroup; import android.widget.TextView; import com.marakana.android.fibonaccicommon.FibonacciRequest; import com.marakana.android.fibonaccicommon.FibonacciResponse; import com.marakana.android.fibonaccicommon.IFibonacciService; import com.marakana.android.fibonaccicommon.IFibonacciServiceResponseListener; public class FibonacciActivity extends Activity implements OnClickListener, ServiceConnection { private static final String TAG = "FibonacciActivity"; // the id of a message to our response handler private static final int RESPONSE_MESSAGE_ID = 1; // the id of a progress dialog that we'll be creating private static final int PROGRESS_DIALOG_ID = 1; private EditText input; // our input n private Button button; // trigger for fibonacci calcualtion private RadioGroup type; // fibonacci implementation type private TextView output; // destination for fibonacci result private IFibonacciService service; // reference to our service // the responsibility of the responseHandler is to take messages // from the responseListener (defined below) and display their content // in the UI thread private final Handler responseHandler = new Handler() { @Override public void handleMessage(Message message) { switch (message.what) { case RESPONSE_MESSAGE_ID: Log.d(TAG, "Handling response"); FibonacciActivity.this.output.setText((String) message.obj); FibonacciActivity.this.removeDialog(PROGRESS_DIALOG_ID); break; } } }; // the responsibility of the responseListener is to receive call-backs // from the service when our FibonacciResponse is available private final IFibonacciServiceResponseListener responseListener = new IFibonacciServiceResponseListener.Stub() { // this method is executed on one of the pooled binder threads @Override public void onResponse(FibonacciResponse response) throws RemoteException { String result = String.format("%d in %d ms", response.getResult(), response.getTimeInMillis()); Log.d(TAG, "Got response: " + result); // since we cannot update the UI from a non-UI thread, // we'll send the result to the responseHandler (defined above) Message message = FibonacciActivity.this.responseHandler .obtainMessage(RESPONSE_MESSAGE_ID, result); FibonacciActivity.this.responseHandler.sendMessage(message); } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); super.setContentView(R.layout.main); // connect to our UI elements this.input = (EditText) super.findViewById(R.id.input); this.button = (Button) super.findViewById(R.id.button); this.type = (RadioGroup) super.findViewById(R.id.type); this.output = (TextView) super.findViewById(R.id.output); // request button click call-backs via onClick(View) method this.button.setOnClickListener(this); // the button will be enabled once we connect to the service this.button.setEnabled(false); } @Override protected void onStart() { Log.d(TAG, "onStart()'ed"); super.onStart(); // Bind to our FibonacciService service, by looking it up by its name // and passing ourselves as the ServiceConnection object // We'll get the actual IFibonacciService via a callback to // onServiceConnected() below if (!super.bindService(new Intent(IFibonacciService.class.getName()), this, BIND_AUTO_CREATE)) { Log.w(TAG, "Failed to bind to service"); } } @Override protected void onStop() { Log.d(TAG, "onStop()'ed"); super.onStop(); // No need to keep the service bound (and alive) any longer than // necessary super.unbindService(this); } public void onServiceConnected(ComponentName name, IBinder service) { Log.d(TAG, "onServiceConnected()'ed to " + name); // finally we can get to our IFibonacciService this.service = IFibonacciService.Stub.asInterface(service); // enable the button, because the IFibonacciService is initialized this.button.setEnabled(true); } public void onServiceDisconnected(ComponentName name) { Log.d(TAG, "onServiceDisconnected()'ed to " + name); // our IFibonacciService service is no longer connected this.service = null; // disabled the button, since we cannot use IFibonacciService this.button.setEnabled(false); } @Override protected Dialog onCreateDialog(int id) { switch (id) { case PROGRESS_DIALOG_ID: // this dialog will be opened in onClick(...) and // dismissed/removed by responseHandler.handleMessage(...) ProgressDialog dialog = new ProgressDialog(this); dialog.setMessage(super.getText(R.string.progress_text)); dialog.setIndeterminate(true); return dialog; default: return super.onCreateDialog(id); } } // handle button clicks public void onClick(View view) { // parse n from input (or report errors) final long n; String s = this.input.getText().toString(); if (TextUtils.isEmpty(s)) { return; } try { n = Long.parseLong(s); } catch (NumberFormatException e) { this.input.setError(super.getText(R.string.input_error)); return; } // build the request object final FibonacciRequest.Type type; switch (FibonacciActivity.this.type.getCheckedRadioButtonId()) { case R.id.type_fib_jr: type = FibonacciRequest.Type.RECURSIVE_JAVA; break; case R.id.type_fib_ji: type = FibonacciRequest.Type.ITERATIVE_JAVA; break; case R.id.type_fib_nr: type = FibonacciRequest.Type.RECURSIVE_NATIVE; break; case R.id.type_fib_ni: type = FibonacciRequest.Type.ITERATIVE_NATIVE; break; default: return; } final FibonacciRequest request = new FibonacciRequest(n, type); try { Log.d(TAG, "Submitting request..."); long time = SystemClock.uptimeMillis(); // submit the request; the response will come to responseListener this.service.fib(request, this.responseListener); time = SystemClock.uptimeMillis() - time; Log.d(TAG, "Submited request in " + time + " ms"); // this dialog will be dismissed/removed by responseHandler super.showDialog(PROGRESS_DIALOG_ID); } catch (RemoteException e) { Log.wtf(TAG, "Failed to communicate with the service", e); } } }
Create an AIDL-described ILogService that provides the following functionality:
package com.marakana.android.logservice; public interface ILogService { public void log(LogMessage logMessage); }
where LogMessage is defined as follows:
package com.marakana.android.logservice; public class LogMessage … { … public LogMessage(int priority, String tag, String msg) { … } … }
Create a simple Android client that allows the user to submit a LogMessage request to the remote ILogService running in a separate process.
You can borrow code (UI) from the basic LogNative application, which you can get:
|
|
Your implementation could simply use android.util.Log.println(int priority, String tag, String msg) to do the logging. |
The solution is provided:
Why do we need IPC on Android?
What is Binder’s unit of data called?
What is AIDL and when do we use it?
Is the use of AIDL required when consuming or exposing bound services?
Name at least five supported method parameter/return data types that we use in AIDL.
Name at least three types that are not supported.
What is android.os.Bundle?
What is android.os.Parcelable and how do we implement it?
What do we have to do with android.os.Parcelable classes before we can use them in AIDL interfaces?
What is android.os.IBinder?
What is special about file descriptors?
What is the purpose of the directional flag?
What is the purpose of the aidl tool?
What is the purpose of the Stub and the Proxy?
What is the Android library project and when do we use them?
What is the purpose of android.app.Service in the context of bound services?
How do we expose a bound service to other applications?
What is the thread context of an binder service request?
How and when do we bind to non-system services?
What is the life-cycle dependency between bound services and their clients?
What is the purpose of the oneway keyword and when do we use it?
What do we have to worry about when handling call-backs from remote services?
$ keytool -genkey -v -keystore marakana.keystore -alias android -keyalg RSA -keysize 2048 -validity 10000 \ -dname "CN=Android Application Signer, OU=Android, O=Marakana Inc., L=San Francisco, ST=California, C=US"
$ jarsigner -keystore marakana.keystore MyApp.apk android $ jarsigner -verify MyApp.apk jar verified.
$ jar -xvf MyApp.apk META-INF/CERT.RSA
$ keytool -printcert -file META-INF/CERT.RSA
Owner: CN=Android Application Signer, OU=Android, O=Marakana Inc., L=San Francisco, ST=California, C=US
Issuer: CN=Android Application Signer, OU=Android, O=Marakana Inc., L=San Francisco, ST=California, C=US
Serial number: 4f4e6461
Valid from: Wed Feb 29 11:46:09 CST 2012 until: Sun Feb 22 11:46:09 CST 2037
Certificate fingerprints:
MD5: 57:B8:23:06:09:CD:2A:C9:9C:02:6C:B4:5D:1C:34:BD
SHA1: 2E:49:E4:20:81:9E:A3:E9:91:5D:99:57:2A:A6:88:0E:23:14:63:AA
Signature algorithm name: SHA1withRSA
Version: 3
$ rm META-INF/CERT.RSA && rmdir META-INF
$ zipalign -v 4 UnalignedApp.apk AlignedApp.apk
$ rm build/target/product/security/platform.p*
$ SIGNER="/C=US/ST=California/L=San Francisco/O=Marakana Inc./OU=Android/CN=Android Platform Signer/emailAddress=android@marakana.com"
$ echo | development/tools/make_key build/target/product/security/platform "$SIGNER"
Repeat for media, shared, and testkey
$ java -jar /path/to/aosp/out/host/linux-x86/framework/signapk.jar \
/path/to/aosp/build/target/product/security/platform.x509.pem \
/path/to/aosp/build/target/product/security/platform.pk8 \
MyApp.apk MySignedApp.apk
$ adb -e shell cat /data/system/packages.list |grep com.android.browser com.android.browser 10001 0 /data/data/com.android.browser
$ adb -e shell ps |grep com.android.browser app_1 682 37 192592 53144 ffffffff 40011384 S com.android.browser
adb -e shell ls -l /data/data/com.android.browser drwxrwx--x app_1 app_1 2012-02-06 12:47 app_appcache drwxrwx--x app_1 app_1 2012-02-06 12:47 app_databases drwxrwx--x app_1 app_1 2012-02-06 12:47 app_geolocation drwxrwx--x app_1 app_1 2012-02-06 12:47 app_icons drwxrwx--x app_1 app_1 2012-02-06 12:47 cache drwxrwx--x app_1 app_1 2012-02-06 12:48 databases drwxr-xr-x system system 2012-01-17 15:42 lib drwxrwx--x app_1 app_1 2012-02-06 12:47 shared_prefs
<manifest package="com.marakana.android.myapp" android:sharedUserId="marakana.uid.myapp" android:sharedUserLabel="@string/myapp_uid" … > <application android:process="myapp" … > … </application> </manifest>
|
|
From the security standpoint, packages with the same sharedUserId are treated as being parts of the same application, with the same UID and file permissions. |
$ adb shell mount rootfs / rootfs ro 0 0 tmpfs /dev tmpfs rw,nosuid,mode=755 0 0 devpts /dev/pts devpts rw,mode=600 0 0 proc /proc proc rw 0 0 sysfs /sys sysfs rw 0 0 none /acct cgroup rw,cpuacct 0 0 tmpfs /mnt/asec tmpfs rw,mode=755,gid=1000 0 0 tmpfs /mnt/obb tmpfs rw,mode=755,gid=1000 0 0 none /dev/cpuctl cgroup rw,cpu 0 0 /dev/block/mtdblock0 /system yaffs2 ro 0 0 /dev/block/mtdblock1 /data yaffs2 rw,nosuid,nodev 0 0 /dev/block/mtdblock2 /cache yaffs2 rw,nosuid,nodev 0 0 /dev/block/vold/179:0 /mnt/sdcard vfat rw,dirsync,nosuid,nodev,noexec,uid=1000,gid=1015,fmask=0702,dmask=0702,allow_utime=0020,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0 /dev/block/vold/179:0 /mnt/secure/asec vfat rw,dirsync,nosuid,nodev,noexec,uid=1000,gid=1015,fmask=0702,dmask=0702,allow_utime=0020,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0 tmpfs /mnt/sdcard/.android_secure tmpfs ro,size=0k,mode=000 0 0
$ adb shell pm path com.android.browser package:/system/app/Browser.apk
$ adb shell run-as com.example.helloworld am start -a android.intent.action.CALL -d tel:4155551234
Starting: Intent { act=android.intent.action.CALL dat=tel:xxxxxxxxxx }
java.lang.SecurityException: Permission Denial: starting Intent { act=android.intent.action.CALL dat=tel:xxxxxxxxxx flg=0x10000000 cmp=com.android.phone/.OutgoingCallBroadcaster } from null (pid=1533, uid=10045) requires android.permission.CALL_PHONE
at android.os.Parcel.readException(Parcel.java:1327)
at android.os.Parcel.readException(Parcel.java:1281)
at android.app.ActivityManagerProxy.startActivity(ActivityManagerNative.java:1631)
at com.android.commands.am.Am.runStart(Am.java:433)
at com.android.commands.am.Am.run(Am.java:107)
at com.android.commands.am.Am.main(Am.java:80)
at com.android.internal.os.RuntimeInit.finishInit(Native Method)
at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:238)
at dalvik.system.NativeStart.main(Native Method)
$ adb shell run-as com.example.helloworld ls /data/misc/keystore opendir failed, Permission denied
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.marakana.android.trackapp"> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.SEND_SMS" /> … </manifest>
$ adb shell pm list permissions All Permissions: permission:android.permission.CLEAR_APP_USER_DATA permission:android.permission.SHUTDOWN permission:android.permission.BIND_INPUT_METHOD permission:android.permission.ACCESS_DRM permission:android.permission.DOWNLOAD_CACHE_NON_PURGEABLE permission:android.permission.INTERNAL_SYSTEM_WINDOW permission:android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS permission:android.permission.MOVE_PACKAGE permission:android.permission.ACCESS_CHECKIN_PROPERTIES permission:android.permission.CRYPT_KEEPER permission:android.permission.READ_INPUT_STATE permission:android.permission.DEVICE_POWER permission:android.permission.DELETE_PACKAGES permission:android.permission.ACCESS_CACHE_FILESYSTEM permission:android.permission.REBOOT permission:android.permission.STATUS_BAR permission:android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED permission:android.permission.ACCESS_ALL_DOWNLOADS permission:android.permission.STOP_APP_SWITCHES permission:android.permission.BIND_VPN_SERVICE permission:android.permission.CONTROL_LOCATION_UPDATES permission:android.permission.ACCESS_DOWNLOAD_MANAGER permission:android.permission.MANAGE_APP_TOKENS permission:android.permission.BIND_PACKAGE_VERIFIER permission:android.permission.DELETE_CACHE_FILES permission:android.permission.BATTERY_STATS permission:android.permission.COPY_PROTECTED_DATA permission:com.android.email.permission.ACCESS_PROVIDER permission:android.permission.INSTALL_DRM permission:android.permission.MASTER_CLEAR permission:android.permission.SET_ACTIVITY_WATCHER permission:android.permission.BRICK permission:android.permission.MODIFY_NETWORK_ACCOUNTING permission:android.permission.READ_NETWORK_USAGE_HISTORY permission:android.permission.BACKUP permission:android.permission.SET_TIME permission:android.permission.STATUS_BAR_SERVICE permission:android.permission.INSTALL_PACKAGES permission:android.permission.PERFORM_CDMA_PROVISIONING permission:android.permission.INJECT_EVENTS permission:android.permission.SET_POINTER_SPEED permission:com.android.browser.permission.PRELOAD permission:android.permission.WRITE_SECURE_SETTINGS permission:android.permission.INSTALL_LOCATION_PROVIDER permission:android.permission.CONFIRM_FULL_BACKUP permission:android.permission.PACKAGE_USAGE_STATS permission:android.permission.ACCESS_SURFACE_FLINGER permission:android.permission.CALL_PRIVILEGED permission:android.permission.PACKAGE_VERIFICATION_AGENT permission:android.permission.CHANGE_COMPONENT_ENABLED_STATE permission:android.intent.category.MASTER_CLEAR.permission.C2D_MESSAGE permission:android.permission.WRITE_GSERVICES permission:android.permission.MANAGE_NETWORK_POLICY permission:android.permission.ALLOW_ANY_CODEC_FOR_PLAYBACK permission:android.permission.BIND_TEXT_SERVICE permission:android.permission.READ_FRAME_BUFFER permission:android.permission.FORCE_BACK permission:android.permission.UPDATE_DEVICE_STATS permission:android.permission.BIND_WALLPAPER permission:android.permission.BIND_REMOTEVIEWS permission:android.permission.SET_ORIENTATION permission:android.permission.FACTORY_TEST permission:android.permission.BIND_DEVICE_ADMIN
|
|
Add -f for the full description of permissions - i.e. adb shell pm list permissions -f |
Using the following permissions will significantly lower the likelihood for an Android app/game to be featured in Google Play (from Google I/O 2012):
android.permission.SEND_SMS and android.permission.RECEIVE_SMS
android.permission.SYSTEM_ALERT_WINDOW
com.android.browser.permission.READ_HISTORY_BOOKMARKS and com.android.browser.permission.WRITE_HISTORY_BOOKMARKS
android.permission.READ_CONTACTS, android.permission.WRITE_CONTACTS, android.permission.READ_CALENDAR, android.permission.WRITE_CALENDAR
android.permission.CALL_PHONE
android.permission.READ_LOGS
android.permission.ACCESS_FINE_LOCATION
android.permission.GET_TASKS
android.permission.RECEIVE_BOOT_COMPLETED
android.permission.CHANGE_WIFI_STATE
... public class MyActivity extends Activity { private static final int CAPTURE_IMAGE_REQ = 1; ... public void onClick(View view) { Intent intent = new Intent( android.provider.MediaStore.ACTION_IMAGE_CAPTURE, CAPTURE_IMAGE_REQ); intent.putExtra( android.provider.MediaStore.EXTRA_OUTPUT, "/sdcard/myphoto.jpeg"); super.startActivityForResult(intent); } protected void onActivityResult (int requestCode, int resultCode, Intent data) { switch (requestCode) { case CAPTURE_IMAGE_REQ: if (resultCode == RESULT_OK) { // we have the image! } break; } } }
... public class MyActivity extends Activity { ... public void onClick(View view) { Uri smsNumber = Uri.parse("sms:14155551234"); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(smsNumber); intent.putExtra(Intent.EXTRA_TEXT, "Hello"); super.startActivity(intent); } }
... public class MyActivity extends Activity { private static final int GET_CONTACT_REQ = 1; ... public void onClick(View view) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType(Phone.CONTENT_ITEM_TYPE); super.startActivityForResult(intent, GET_CONTACT_REQ); } public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == GET_CONTACT_REQ && resultCode == RESULT_OK && data != null) { Uri uri = data.getData(); if (uri != null) { try { // best to do on another thread Cursor c = getContentResolver().query(uri, new String[] {Contacts.DISPLAY_NAME, Phone.NUMBER}, null, null, null); if (c.moveToFirst()) { String name = c.getString(0); String phone = c.getString(1); // we have the contact info! } } catch (RuntimeException e) { // handle } } } } }
String deviceId = UUID.randomUUID().toString(); // store deviceId in the application preferences
|
|
For more choices on how to get a unique device ID, see http://android-developers.blogspot.com/2011/03/identifying-app-installations.html |
There are a number of trigger points for security/permission checks:
<permissions> … <permission name="android.permission.INTERNET" > <group gid="inet" /> </permission> <permission name="android.permission.CAMERA" > <group gid="camera" /> </permission> <permission name="android.permission.READ_LOGS" > <group gid="log" /> </permission> <permission name="android.permission.WRITE_EXTERNAL_STORAGE" > <group gid="sdcard_rw" /> </permission> … </permissions>
|
|
Run adb shell cat /system/etc/permissions/platform.xml to see all the mappings |
… static struct { unsigned uid; const char *name; } allowed[] = { #ifdef LVMX { AID_MEDIA, "com.lifevibes.mx.ipc" }, #endif { AID_MEDIA, "media.audio_flinger" }, { AID_MEDIA, "media.player" }, { AID_MEDIA, "media.camera" }, { AID_MEDIA, "media.audio_policy" }, { AID_DRM, "drm.drmManager" }, { AID_NFC, "nfc" }, { AID_RADIO, "radio.phone" }, { AID_RADIO, "radio.sms" }, { AID_RADIO, "radio.phonesubinfo" }, { AID_RADIO, "radio.simphonebook" }, /* TODO: remove after phone services are updated: */ { AID_RADIO, "phone" }, { AID_RADIO, "sip" }, { AID_RADIO, "isms" }, { AID_RADIO, "iphonesubinfo" }, { AID_RADIO, "simphonebook" }, }; … int svc_can_register(unsigned uid, uint16_t *name) { unsigned n; if ((uid == 0) || (uid == AID_SYSTEM)) return 1; for (n = 0; n < sizeof(allowed) / sizeof(allowed[0]); n++) if ((uid == allowed[n].uid) && str16eq(name, allowed[n].name)) return 1; return 0; } …
<manifest …> … <application …> … <activity android:name=".GetPasswordActivity" android:permission="com.marakana.android.permission.GET_PASSWORD_FROM_USER" … > … </activity> <service android:name=".UserAuthenticatorService" android:permission="com.marakana.android.permission.AUTHENTICATE_USER" … > … </service> <provider android:name=".EnterpriseDataProvider" android:readPermission="com.marakana.android.permission.READ_ENTERPRISE_DATA" android:writePermission="com.marakana.android.permission.WRITE_ENTERPRISE_DATA" … > … </provider> <receiver android:name=".UserAuthStatusReceiver" android:permission="com.marakana.android.permission.SEND_USER_AUTH_STATUS"> … </receiver> </application> </manifest>
|
|
If the permission is defined in the same application (which is the most usual case), then we can statically access its name via the auto-generated Manifest class. For example: Manifest.permission.MY_PERMISSION |
package com.android.server; … public class VibratorService extends IVibratorService.Stub { … public void vibrate(long milliseconds, IBinder token) { if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.VIBRATE) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires VIBRATE permission"); } … } … }
package com.android.server; … public class LocationManagerService extends ILocationManager.Stub implements Runnable { … private static final String ACCESS_FINE_LOCATION = android.Manifest.permission.ACCESS_FINE_LOCATION; private static final String ACCESS_COARSE_LOCATION = android.Manifest.permission.ACCESS_COARSE_LOCATION; … private void checkPermissionsSafe(String provider) { if ((LocationManager.GPS_PROVIDER.equals(provider) || LocationManager.PASSIVE_PROVIDER.equals(provider)) && (mContext.checkCallingOrSelfPermission(ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED)) { throw new SecurityException("Provider " + provider + " requires ACCESS_FINE_LOCATION permission"); } if (LocationManager.NETWORK_PROVIDER.equals(provider) && (mContext.checkCallingOrSelfPermission(ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) && (mContext.checkCallingOrSelfPermission(ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)) { throw new SecurityException("Provider " + provider + " requires ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION permission"); } } … private Location _getLastKnownLocationLocked(String provider) { checkPermissionsSafe(provider); … } … public Location getLastKnownLocation(String provider) { … _getLastKnownLocationLocked(provider); … } }
Before we can enforce our own permissions, we have to declare them using one or more <permission> in
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.marakana.android.myapp" > <permission android:name="com.example.app.DO_X" android:label="@string/do_x_label" android:description="@string/do_x_desc" android:permissionGroup="android.permission-group.PERSONAL_INFO" android:protectionLevel="dangerous" /> … </manifest>
$ adb shell pm list permission-groups permission group:android.permission-group.DEVELOPMENT_TOOLS permission group:android.permission-group.PERSONAL_INFO permission group:android.permission-group.COST_MONEY permission group:android.permission-group.LOCATION permission group:android.permission-group.MESSAGES permission group:android.permission-group.NETWORK permission group:android.permission-group.ACCOUNTS permission group:android.permission-group.STORAGE permission group:android.permission-group.PHONE_CALLS permission group:android.permission-group.HARDWARE_CONTROLS permission group:android.permission-group.SYSTEM_TOOLS
|
|
We can list permissions currently defined an an Android device/emulator: $ adb shell pm list permissions -s |
<manifest …> … <permission-tree android:name="com.marakana.android.foo" /> … </manifest>
… public class FooActivity extends Activity … { public void onCreate() { super.onCreate(); … PermissionInfo permission = new PermissionInfo(); permission.name = "com.marakana.android.foo.DO_X_WITH_FOO"; permission.protectionLevel = PermissionInfo.PROTECTION_SIGNATURE; super.getPackageManager().addPermission(permission); … } }
Here, we want to restrict access to the com.marakana.android.fibonacciservice.FibonacciService to applications (i.e. clients) that hold USE_FIBONACCI_SERVICE custom permission
We start by by creating a custom permission group (making sure that we name-space it):
<?xml version="1.0" encoding="utf-8"?> <resources> … <string name="fibonacci_permissions_group_label">Fibonacci Permissions</string> … </resources>
<?xml version="1.0" encoding="utf-8"?> <manifest …> … <permission-group android:name="com.marakana.android.fibonacciservice.FIBONACCI_PERMISSIONS" android:label="@string/fibonacci_permissions_group_label" /> … </manifest>
|
|
This permission group is optional - as we could instead use one of the already provided groups |
Next, we create a custom permission (again, making sure that we name-space it), while taking advantage of our newly-created permission group:
<?xml version="1.0" encoding="utf-8"?> <resources> … <string name="use_fibonacci_service_permission_label">use fibonacci service</string> <string name="use_fibonacci_service_permission_description"> applications with this permissions get fibonacci results for free </string> … </resources>
<?xml version="1.0" encoding="utf-8"?> <manifest …> … <permission-group …/> <permission android:name="com.marakana.android.fibonacciservice.USE_FIBONACCI_SERVICE" android:description="@string/use_fibonacci_service_permission_description" android:label="@string/use_fibonacci_service_permission_label" android:permissionGroup="com.marakana.android.fibonacciservice.FIBONACCI_PERMISSIONS" android:protectionLevel="dangerous" /> … </manifest>
Now we can statically require the permission on our FibonacciService service:
<?xml version="1.0" encoding="utf-8"?> <manifest …> … <permission-group …/> <permission …/> <application …> <service android:name=".FibonacciService" android:permission="com.marakana.android.fibonacciservice.USE_FIBONACCI_SERVICE" > … </service> </application> … </manifest>
If we now re-run the FibonacciService and re-run the FibonacciClient, we will notice that the client will fail to launch and adb logcat will show something like:
…
W/ActivityManager( 85): Permission Denial: Accessing service ComponentInfo{com.marakana.android.fibonacciservice/com.marakana.android.fibonacciservice.FibonacciService} from pid=540, uid=10043 requires com.marakana.android.fibonacciservice.USE_FIBONACCI_SERVICE
D/AndroidRuntime( 540): Shutting down VM
W/dalvikvm( 540): threadid=1: thread exiting with uncaught exception (group=0x409c01f8)
E/AndroidRuntime( 540): FATAL EXCEPTION: main
E/AndroidRuntime( 540): java.lang.RuntimeException: Unable to resume activity {com.marakana.android.fibonacciclient/com.marakana.android.fibonacciclient.FibonacciActivity}: java.lang.SecurityException: Not allowed to bind to service Intent { act=com.marakana.android.fibonaccicommon.IFibonacciService }
E/AndroidRuntime( 540): at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2444)
…
E/AndroidRuntime( 540): at dalvik.system.NativeStart.main(Native Method)
E/AndroidRuntime( 540): Caused by: java.lang.SecurityException: Not allowed to bind to service Intent { act=com.marakana.android.fibonaccicommon.IFibonacciService }
E/AndroidRuntime( 540): at android.app.ContextImpl.bindService(ContextImpl.java:1135)
E/AndroidRuntime( 540): at android.content.ContextWrapper.bindService(ContextWrapper.java:370)
E/AndroidRuntime( 540): at com.marakana.android.fibonacciclient.FibonacciActivity.onResume(FibonacciActivity.java:65)
…
W/ActivityManager( 85): Force finishing activity com.marakana.android.fibonacciclient/.FibonacciActivity
…
Finally, we can give FibonacciClient a fighting chance by allowing it to use the USE_FIBONACCI_SERVICE permission:
<?xml version="1.0" encoding="utf-8"?> <manifest …> … <uses-permission android:name="com.marakana.android.fibonacciservice.USE_FIBONACCI_SERVICE"/> … </manifest>
We can now observe that our client is again able to use the service
In the Emulator, if we go to Home → Menu → Manage apps → Fibonacci Client → PERMISSIONS, we should see the Fibonacci Permissions group and under it, use fibonacci service permission
Here, we want to restrict access to the com.marakana.android.fibonacciservice.IFibonacciServiceImpl's recursive operations (fibJR(long n) and fibNR(long n)) for n > 10 to applications (i.e. clients) that hold USE_SLOW_FIBONACCI_SERVICE custom permission
Like before, we start off by creating a custom permission:
<?xml version="1.0" encoding="utf-8"?> <resources> … <string name="use_slow_fibonacci_service_permission_label"> use slow fibonacci service operations </string> <string name="use_slow_fibonacci_service_permission_description"> applications with this permissions can melt the CPU and drain the battery by using slow fibonacci operations </string> … </resources>
<?xml version="1.0" encoding="utf-8"?> <manifest …> … <permission-group …/> <permission …/> <permission android:name="com.marakana.android.fibonacciservice.USE_SLOW_FIBONACCI_SERVICE" android:description="@string/use_slow_fibonacci_service_permission_description" android:label="@string/use_slow_fibonacci_service_permission_label" android:permissionGroup="com.marakana.android.fibonacciservice.FIBONACCI_PERMISSIONS" android:protectionLevel="dangerous" /> … </manifest>
Next, we update our IFibonacciServiceImpl to enforce this permission dynamically - via a android.content.Context that get expect to get through the constructor:
package com.marakana.android.fibonacciservice; import android.content.Context; … public class IFibonacciServiceImpl extends IFibonacciService.Stub { … private final Context context; public IFibonacciServiceImpl(Context context) { this.context = context; } private long checkN(long n) { if (n > 10) { this.context.enforceCallingOrSelfPermission( Manifest.permission.USE_SLOW_FIBONACCI_SERVICE, "Go away!"); } return n; } … public long fibJR(long n) { … return FibLib.fibJR(this.checkN(n)); } … public long fibNR(long n) { … return FibLib.fibNR(this.checkN(n)); } … }
We have to update FibonacciService to invoke the new IFibonacciServiceImpl's constructor:
… public class FibonacciService extends Service { … @Override public void onCreate() { … this.service = new IFibonacciServiceImpl(super.getApplicationContext()); … } … }
If we now re-run the FibonacciService and re-run the FibonacciClient for a recursive operation with n > 10, we will notice that the client will fail and adb logcat will show something like:
… D/IFibonacciServiceImpl( 617): fib(15, RECURSIVE_NATIVE) D/IFibonacciServiceImpl( 617): fibNR(15) W/dalvikvm( 604): threadid=11: thread exiting with uncaught exception (group=0x409c01f8) E/AndroidRuntime( 604): FATAL EXCEPTION: AsyncTask #1 E/AndroidRuntime( 604): java.lang.RuntimeException: An error occured while executing doInBackground() … E/AndroidRuntime( 604): at java.lang.Thread.run(Thread.java:856) E/AndroidRuntime( 604): Caused by: java.lang.SecurityException: Go away!: Neither user 10043 nor current process has com.marakana.android.fibonacciservice.USE_SLOW_FIBONACCI_SERVICE. …
Finally, we can allow FibonacciClient to melt our CPU and drain our battery by allowing it to use the USE_SLOW_FIBONACCI_SERVICE permission:
<?xml version="1.0" encoding="utf-8"?> <manifest …> … <uses-permission android:name="com.marakana.android.fibonacciservice.USE_SLOW_FIBONACCI_SERVICE"/> … </manifest>
We can now observe that our client is again able to use recursive fibonacci operations even for n > 10
In the Emulator, if we go to Home → Menu → Manage apps → Fibonacci Client → PERMISSIONS → Fibonacci Permissions, we should see both use fibonacci service and use slow fibonacci service operations permissions
package com.marakana.android.logcommon; import com.marakana.android.logcommon.LogMessage; interface ILogService { void log(in LogMessage logMessage); }
package com.marakana.android.logcommon; import android.os.Parcel; import android.os.Parcelable; public class LogMessage implements Parcelable { private final int priority; private final String tag; private final String msg; public LogMessage(int priority, String tag, String msg) { this.priority = priority; this.tag = tag; this.msg = msg; } public int getPriority() { return priority; } public String getTag() { return tag; } public String getMsg() { return msg; } … }
Get LogCommon, LogService, and LogClient Eclipse projects
Unzip or git clone into your workspace directory
Import LogCommon, LogClient, and LogService (as existing) projects into Eclipse
|
|
If you get errors on import, try to clean all projects (menubar → Project → Clean… → Clean all projects → OK), and/or close and reopen all projects, and/or restart Eclipse |
Restrict access to the com.marakana.android.logservice.LogService to applications that hold USE_LOG_SERVICE custom permission
Create a custom permission group (make sure to name-space it)
Create a custom permission (make sure to name-space it)
Then require the permission on the service
Test that a client without the required permission cannot bind to the service
Look for an exception stack trace in adb logcat when you launch the client
Have the client use the required permission
Test again - the client should now be able to bind to the service as before
Restrict access to "long" log messages on com.marakana.android.logservice.ILogService to applications that hold USE_LONG_LOG_SERVICE permission (e.g. where "long" == logMessage.getTag().length() > 10 || logMessage.getMsg().length() > 80)
Create another custom permission (make sure to name-space it)
Dynamically enforce your permission
|
|
For you to do this, you’ll need access to a android.content.Context object inside the provided com.marakana.android.logservice.ILogServiceImpl. Conveniently enough, com.marakana.android.logservice.LogService extends android.app.Service, which in turn implements android.content.Context and has access to application context via getApplicationContext() method. |
Test that a client without the required permission cannot log "long" messages
Have the client use the required permission
Test again - the client should now be able to use the service as before
<manifest …> … <application …> … <activity android:name=".GetPasswordActivity" android:exported="false" … > <intent-filter> … </intent-filter> </activity> </application> </manifest>
|
|
Most exported components should be protected with permissions. |
What is the basic philosophy of the Android security model?
What does Android do to implement this philosophy?
How and when are permissions used, granted, and enforced?
What is a confused deputy attack?
What is a collusion attack?
Which part of Android is ultimately responsible for providing the system security?
How does native code differ from Java code when it comes to its security context?
Why do we sign applications?
What are the four platform keys?
What is the relationship between apps and Linux user IDs?
When are the user IDs assigned to apps?
Where are the user ID assignments recorded?
What is the relationship between application user IDs and its file-system resources?
Where is the home of an app with the package name com.foo.bar?
How do apps share a user IDs?
How do apps share a process? Provide at least one use-case for this.
Name at least five (file) system volumes (think mount points).
What is the purpose of /data/dalvik-cache/?
What is the purpose of run-as command?
How do you use permissions?
What happens if you don’t use a permission but you attempt to use a restricted API?
How are the permissions enforced?
How do bound services enforce their permissions? What makes that possible?
What are the attributes of the <permission … /> tag?
What are the possible values for protectionLevel attribute?
What does protectedLevel=signatureOrSystem mean?
What is the value of grouping permissions?
What is the significance of android:grantUriPermission attribute on content providers?
What is the significance android:exported attribute on Android components?
How do you protected unauthorized applications from receiving your broadcasts (intents)?
What’s the worry with pending intents?
Add the following to:
… * soft nofile 8192 * hard nofile 8192
Reboot
$ mkdir ~/bin $ export PATH=~/bin:$PATH $ curl https://dl-ssl.google.com/dl/googlesource/git-repo/repo > ~/bin/repo $ chmod a+x ~/bin/repo
$ mkdir android-src $ cd android-src
$ repo init -u https://android.googlesource.com/platform/manifest.git -b android-4.1.1_r1
$ git clone https://android.googlesource.com/platform/manifest.git $ cd manifest $ git branch -a
|
|
Official Android repository android.googlesource.com is not the only game in town. For example, to get Android sources for TI’s SOCs, we would: repo init -u git://gitorious.org/rowboat/manifest.git -m TI-Android-FroYo-DevKit-V2.2.xml for OMAP3 and repo init -u git://gitorious.org/rowboat/manifest.git -m TI-Android-FroYo-DSP-DevKit-V2.xml for DM37x EVM (the .xml files come from TI) to check out Froyo build optimized for their hardware. |
$ repo sync
$ source build/envsetup.sh $ help Invoke ". build/envsetup.sh" from your shell to add the following functions to your environment: - croot: Changes directory to the top of the tree. - m: Makes from the top of the tree. - mm: Builds all of the modules in the current directory. - mmm: Builds all of the modules in the supplied directories. - cgrep: Greps on all local C/C++ files. - jgrep: Greps on all local Java files. - resgrep: Greps on all local res/*.xml files. - godir: Go to the directory containing a file. Look at the source to view more functions. The complete list is: add_lunch_combo cgrep check_product check_variant choosecombo chooseproduct choosetype choosevariant cproj croot findmakefile gdbclient get_abs_build_var get_build_var getbugreports getprebuilt gettop godir help isviewserverstarted jgrep lunch m mm mmm pid print_lunch_menu printconfig resgrep runhat runtest set_java_home set_sequence_number set_stuff_for_environment setpaths settitle smoketest startviewserver stopviewserver systemstack tapas tracedmdump
$ lunch
You're building on linux
Lunch menu... pick a combo:
1. generic-eng
2. full_passion-userdebug
3. full_crespo-userdebug
4. full_crespo4g-userdebug
Which would you like? [generic-eng]
============================================
PLATFORM_VERSION_CODENAME=REL
PLATFORM_VERSION=2.3.4
TARGET_PRODUCT=generic
TARGET_BUILD_VARIANT=eng
TARGET_SIMULATOR=false
TARGET_BUILD_TYPE=release
TARGET_BUILD_APPS=
TARGET_ARCH=arm
HOST_ARCH=x86
HOST_OS=linux
HOST_BUILD_TYPE=release
BUILD_ID=GINGERBREAD
============================================
$ choosecombo
Only device builds are supported for linux
Forcing TARGET_SIMULATOR=false
Press enter:
Build type choices are:
1. release
2. debug
Which would you like? [1]
Which product would you like? [generic]
Variant choices are:
1. user
2. userdebug
3. eng
Which would you like? [eng]
============================================
PLATFORM_VERSION_CODENAME=REL
PLATFORM_VERSION=2.3.4
TARGET_PRODUCT=generic
TARGET_BUILD_VARIANT=eng
TARGET_SIMULATOR=false
TARGET_BUILD_TYPE=release
TARGET_BUILD_APPS=
TARGET_ARCH=arm
HOST_ARCH=x86
HOST_OS=linux
HOST_BUILD_TYPE=release
BUILD_ID=GINGERBREAD
============================================
|
|
Building AOSP on Linux-x86 also supported a simulator target, which was a partial build of Android, compiled as a single Linux x86 binary, meant to run as a stand-alone process. It would enable testing Dalvik, OpenCORE/Stagefright, or WebKit under Valgrind (instrumentation/debugging framework). But simulator has been dropped as a valid lunch target since it’s no longer actively maintained by the AOSP team. |
|
|
Additional CodeName-BuildType combinations may be available based on what build/envsetup.sh finds (usually in the device/ folder) |
|
|
Building for real hardware usually requires that we get proprietary binaries (mostly user-space HAL). For Nexus and other Google-supported devices, we can go to http://code.google.com/android/nexus/drivers.html, which allows us to download scripts, which in turn extract binaries from the connected devices (via adb pull) into vendor/ directory tree structure. |
$ export USE_CCACHE=1
$ prebuilt/linux-x86/ccache/ccache -M 50G
$ make -j4
|
|
The entire build can take 20-120 mins on decent hardware (i7 with 16GB of RAM) or 5-8 hours on under-powered systems (e.g. an i3 with 4GB of RAM). |
|
|
To find out exactly how long your build takes, consider running it as: time make …. |
GNU Make supports -j<JOBS> argument, which allows us to parallelize compilation across multiple hardware threads. Typically, <JOBS> should be set to the number of hardware threads available on the host machine * 2. For example, use make -j16 to build on a quad-core i7 with two hardware threads per core (i.e. hyperthreaded).
To set <JOBS> dynamically, we could do:
$ HOST_OS=`uname -s`
$ case "$HOST_OS" in
Darwin)
BUILD_NUM_CPUS=$(sysctl -n hw.ncpu)
;;
Linux)
BUILD_NUM_CPUS=$(grep -c processor /proc/cpuinfo)
;;
*)
echo "ERROR: Unsupported OS: $HOST_OS"
BUILD_NUM_CPUS=1
$ esac
$ JOBS=$(( $BUILD_NUM_CPUS * 2 ))
$ make -j$JOBS
|
|
We need to use -j<JOBS> with caution, because we may run out of memory, disk I/O, or file handles before we exhaust the CPU(s). |
Android build system Makefile (actually build/core/main.mk) includes some of the following targets:
| Make targets | Description |
|---|---|
droid |
The default target (build the full system) |
clean |
Equivalent to rm -rf out/ (same as make clobber) |
installclean |
Deletes all of the files that change between different build types, like make user vs. make sdk |
dataclean |
Delete files in the staging and emulator data partitions: data/*, data-qemu/*, and userdata-qemu.img |
snod |
Quickly rebuild the system image from built packages |
offline-sdk-docs |
Generate the HTML for the developer SDK docs |
doc-comment-check-docs |
Check HTML doc links and validity, without generating HTML |
libandroid_runtime |
All the JNI framework stuff |
framework |
All the java framework stuff |
services |
The system server (Java) and friends |
sdk |
Build the Android SDK (tools) |
help |
Display a help message listing some of these targets |
modules |
Display a list of modules that can be built (where each module name is specified by LOCAL_MODULE) |
<module-name> |
Make just a specific module (same as cd module/dir && mm) |
clean-<module-name> |
Clean just a specific module |
otacerts |
OTA keys that are used to verify OTA packages |
… |
Etc. |
52 /* 53 ** +-----------------+ 54 ** | boot header | 1 page 55 ** +-----------------+ 56 ** | kernel | n pages 57 ** +-----------------+ 58 ** | ramdisk | m pages 59 ** +-----------------+ 60 ** | second stage | o pages 61 ** +-----------------+ 62 ** 63 ** n = (kernel_size + page_size - 1) / page_size 64 ** m = (ramdisk_size + page_size - 1) / page_size 65 ** o = (second_size + page_size - 1) / page_size 66 ** 67 ** 0. all entities are page_size aligned in flash 68 ** 1. kernel and ramdisk are required (size != 0) 69 ** 2. second is optional (second_size == 0 -> no second) 70 ** 3. load each element (kernel, ramdisk, second) at 71 ** the specified physical address (kernel_addr, etc) 72 ** 4. prepare tags at tag_addr. kernel_args[] is 73 ** appended to the kernel commandline in the tags. 74 ** 5. r0 = 0, r1 = MACHINE_TYPE, r2 = tags_addr 75 ** 6. if second_size != 0: jump to second_addr 76 ** else: jump to kernel_addr 77 */
$ source build/envsetup.sh $ lunch … $ make … $ $ANDROID_HOST_OUT/bin/emulator &
|
|
The environment variable ANDROID_HOST_OUT is set by lunch and it defaults to out/host/linux-x86 on Linux and out/host/darwin-x86 on Mac OS X. |
$ android create avd --sdcard 16M --target android-10 --name custom-avd --path custom-avd Android 2.3.3 is a basic Android platform. Do you wish to create a custom hardware profile [no] Created AVD 'custom-avd' based on Android 2.3.3, with the following hardware config: hw.lcd.density=240 vm.heapSize=24 hw.ramSize=256
$ emulator -avd custom-avd -system out/target/product/generic/system.img -ramdisk out/target/product/generic/ramdisk.img &
$ adb reboot bootloader
$ out/host/linux-x86/bin/fastboot oem unlock
|
|
Nexus One (passion) does not allow the bootloader to be locked again with fastboot oem lock |
$ out/host/linux-x86/bin/fastboot flashall -w
|
|
Running this command will wipe our device! The -w switch is used to also wipe the /data partition, which is sometimes necessary (on first-flash, or on major updates), but is otherwise not required. |
|
|
Nexus One (passion) does not support the -w switch, so instead we can fastboot erase cache and fastboot erase userdata before flashing. |
|
|
It is easiest to build the Linux kernel on a Linux OS. While other host OSs can also be used, they are not trivial to setup. |
$ git clone https://android.googlesource.com/kernel/common.git $ git clone https://android.googlesource.com/kernel/goldfish.git $ git clone https://android.googlesource.com/kernel/msm.git $ git clone https://android.googlesource.com/kernel/omap.git $ git clone https://android.googlesource.com/kernel/samsung.git $ git clone https://android.googlesource.com/kernel/tegra.git
Start the emulator
Get the existing kernel version from /proc/version (since uname does not exist on Android)
$ adb shell cat /proc/version Linux version 2.6.29-g46b05b2 (vchtchetkine@vc-irv.irv.corp.google.com) (gcc version 4.4.3 (GCC) ) #28 Thu Nov 17 06:39:36 PST 2011
Get the corresponding kernel version (here, we are getting the kernel for goldfish, the emulator)
$ git clone https://android.googlesource.com/kernel/goldfish.git $ cd goldfish/ $ git branch -a $ git checkout -t remotes/origin/android-goldfish-2.6.29
|
|
Alternatively, we could directly clone the goldfish 2.6.29 branch $ git clone https://android.googlesource.com/kernel/goldfish.git -b android-goldfish-2.6.29 |
Specify the target architecture and cross compiler
$ export ARCH=arm $ export CROSS_COMPILE=/path/to/android-src/prebuilt/linux-x86/toolchain/arm-eabi-4.4.3/bin/arm-eabi-
|
|
Android’s own build system uses ARCH and CROSS_COMPILE env vars, so these need to be cleared before building AOSP. As an alternative, these can be also passed directly to the make commands: $ make … ARCH=… CROSS_COMPILE=… |
Get the existing kernel configuration file (with all of the Android/emulator specific options) by pulling it from the running emulator:
$ adb pull /proc/config.gz . $ gunzip config.gz $ mv config .config
|
|
As an alternative, we could generate the Goldfish .config file (even without the emulator) by running: $ make goldfish_armv7_defconfig ARCH=arm |
You can optionally take a look at the existing configuration options and make changes as desired
$ make menuconfig ARCH=arm
Now we are ready to compile
$ make
The resulting kernel will be compressed to arch/arm/boot/zImage
Run the emulator with our new kernel
$ $ANDROID_HOST_OUT/bin/emulator -kernel /path/to/common/arch/arm/boot/zImage
And the result is
What is repo?
What Android’s build system based on?
What is the first thing you need to do before you can build Android (after you get the source)?
What is the unit of build?
What are the different build types and how do they differ?
What is the purpose of ccache?
What is the end result of a build?
What’s inside recovery.img?
What is the purpose of fastboot?
What is the purpose of CROSS_COMPILE variable and what do you set it to when compiling the kernel?
On power-up, CPU is uninitialized - wait for stable power
Execute Boot ROM (hardwired into CPU)
Locate the first-stage boot loader
Load the first-stage boot loader into internal RAM
Jump to first-stage boot loader’s memory location to execute it
First-stage boot loader runs
Detect and initialize external RAM
Locate the second-stage boot loader
Load the second-stage boot loader into external RAM
Jump to the second-stage boot loader’s memory location to execute it
Second-stage boot loader runs
Setup file systems (typically on Flash media)
Optionally setup display, network, additional memory, and other devices
Enable additional CPU features
Enable low-level memory protection
Optionally load security protections (e.g. ROM validation code)
Locate Linux Kernel
Load Linux Kernel into RAM
Place Linux Kernel boot parameters into memory so that kernel knows what to run upon startup
Jump to Linux Kernel memory address to run it
Linux Kernel runs
Build a table in RAM describing the layout of the physical memory
Initialize and setup input devices
Initialize and setup disk (typically MTD) controllers and map available block devices in RAM
Initialize Advanced Power Management (APM) support
Initialize interrupt handlers: Interrupt Descriptor Table (IDT), Global Descriptor Table (GDT), and Programmable Interrupt Controllers (PIC)
Reset the floating-point unit (FPU)
Switch from real to protected mode (i.e. enable memory protection)
Initialize segmentation registers and a provisional stack
Zero uninitialized memory
Decompress the kernel image
Initialize provisional kernel page tables and enable paging
Setup kernel mode stack for process 0
Fill the IDT with null interrupt handlers
Initialize the first page frame with system parameters
Identify the CPU model
Initialize registers with the addresses of the GDT and IDT
Initialize and start the kernel
Scheduler
Memory zones
Buddy system allocator
IDT
SoftIRQs
Date and Time
Slab allocator
…
Create process 1 (/init) and run it
on <trigger> <command> <command> <command>
service <name> <pathname> [ <argument> ]* <option> <option> ...
Starts ueventd
Initializes the system clock and logger
Sets up global environment
Sets up the file system (mount points and symbolic links)
Configures kernel timeouts and scheduler
Configures process groups
Mounts the file systems
Creates a basic directory structure on /data and applies permissions
Applies permissions on /cache
Applies permissions on certain /proc points
Initializes local network (i.e. localhost)
Configures the parameters for the low memory killer [Android_Linux_Kernel_Low_Memory_Killer]
Applies permissions for system_server and daemons
Defines TCP buffer sizes for various networks
Configures and (optionally) loads various daemons (i.e. services): ueventd, console, adbd, servicemanager, vold, netd, debuggerd, rild, zygote (which in turn starts system_server), mediaserver, bootanimation (one time), and various Bluetooth daemons (like dbus-daemon, bluetoothd, etc.), installd, racoon, mtpd, keystore
Sets up product info
Initializes device-driver-specific info, file system structures, and permissions: battery, wifi, phone, uart_switch, GPS, radio, bluetooth, NFC, lights
Initializes and (re)mounts file systems
Loads additional device-specific daemons
Zygote starts from /init.rc
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
socket zygote stream 666
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd
This translates to frameworks/base/cmds/app_process/app_main.cpp:main()
The command app_process then launches frameworks/base/core/java/com/android/internal/os/ZygoteInit.java:main() in a Dalvik VM via frameworks/base/core/jni/AndroidRuntime.cpp:start()
ZygoteInit.main() then
Registers for zygote socket
Pre-loads classes defined in frameworks/base/preloaded-classes (1800+)
Pre-loads resources preloaded_drawables and preloaded_color_state_lists from frameworks/base/core/res/res/values/arrays.xml
Runs garbage collector (to clean the memory as much as possible)
Forks itself to start system_server
Starts listening for requests to fork itself for other apps
When Zygote forks itself to launch the system_server process (in ZygoteInit.java:startSystemServer()), it executes frameworks/base/services/java/com/android/server/SystemServer:java.main()
The SystemServer:java.main() method loads android_servers JNI lib from frameworks/base/services/jni and invokes init1() native method
Before init1() runs, the JNI loader first runs frameworks/base/services/jni/onload.cpp:JNI_OnLoad(), which registers native services - to be used as JNI counterparts to Java-based service manager loaded later
Now frameworks/base/services/jni/com_android_server_SystemServer.cpp:init1() is invoked, which simply wraps a call to frameworks/base/cmds/system_server/library/system_init.cpp:system_init()
The system_init.cpp:system_init() function
First starts native services (some optionally):
frameworks/base/services/surfaceflinger/SurfaceFlinger.cpp
frameworks/base/services/sensorservice/SensorService.cpp
frameworks/base/services/audioflinger/AudioFlinger.cpp
frameworks/base/media/libmediaplayerservice/MediaPlayerService.cpp
frameworks/base/camera/libcameraservice/CameraService.cpp
frameworks/base/services/audioflinger/AudioPolicyService.cpp
Then goes back to frameworks/base/services/java/com/android/server/SystemServer.java:init2(), again via frameworks/base/core/jni/AndroidRuntime.cpp:start() JNI call
The SystemServer.java:init2() method then starts Java service managers in a separate thread (ServerThread), readies them, and registers each one with frameworks/base/core/java/android/os/ServiceManager:addService() (which in turn delegates to to ServiceManagerNative.java, which effectively talks to servicemanager daemon previously started by init)
frameworks/base/services/java/com/android/server/PowerManagerService.java
frameworks/base/services/java/com/android/server/am/ActivityManagerService.java
frameworks/base/services/java/com/android/server/TelephonyRegistry.java
frameworks/base/services/java/com/android/server/PackageManagerService.java
frameworks/base/services/java/com/android/server/BatteryService.java
frameworks/base/services/java/com/android/server/VibratorService.java
etc
Finally frameworks/base/services/java/com/android/server/am/ActivityManagerService.java:finishBooting() sets sys.boot_completed=1 and sends out
a broadcast intent with android.intent.action.PRE_BOOT_COMPLETED action (to give apps a chance to reach to boot upgrades)
an activity intent with android.intent.category.HOME category to launch the Home (or Launcher) application
a broadcast intent with android.intent.action.BOOT_COMPLETED action, which launches applications subscribed to this intent (while using android.permission.RECEIVE_BOOT_COMPLETED)
What is the purpose of the second-stage bootloader?
What is the first thing that runs (in the user-space) after the kernel is initialized?
Where does this process (with PID==1) get its initialization instructions from?
What are actions (of process with PID==1)?
Name at least three triggers and three commands for actions.
What are services (of process with PID==1)?
How are services launched?
What’s the difference between critical and non-critical services?
Name at least five services started by process with PID==1.
What is the purpose of ueventd process?
What is the purpose of zygote process and what does it do upon initialization?
What is the purpose of system_server process and what does it do upon initialization?
Who is responsible for launching the Home application and how is it done?
How do applications that want to start on boot get launched?
The objective of this module is to explain the inter-workings of various Android subsystems. There are close to sixty various services in ICS release. In this module, we’ve hand-picked some of the more common ones. By the end of the module, you should start to understand some common traits of Android subsystem architectures, such as use of the Binder for inter-process communication, and use of JNI for Java-C interaction.
Android services are the key to exposing lower level functionality of the hardware and the Linux kernel to the high level Android apps. Understanding how they work creates the opportunity to customize and extend their behavior, or add another service altogether.
$ adb shell service list Found 56 services: 0 phone: [com.android.internal.telephony.ITelephony] 1 iphonesubinfo: [com.android.internal.telephony.IPhoneSubInfo] 2 simphonebook: [com.android.internal.telephony.IIccPhoneBook] 3 isms: [com.android.internal.telephony.ISms] 4 samplingprofiler: [] 5 diskstats: [] 6 appwidget: [com.android.internal.appwidget.IAppWidgetService] 7 backup: [android.app.backup.IBackupManager] 8 uimode: [android.app.IUiModeManager] 9 usb: [android.hardware.usb.IUsbManager] 10 audio: [android.media.IAudioService] 11 wallpaper: [android.app.IWallpaperManager] 12 dropbox: [com.android.internal.os.IDropBoxManagerService] 13 search: [android.app.ISearchManager] 14 country_detector: [android.location.ICountryDetector] 15 location: [android.location.ILocationManager] 16 devicestoragemonitor: [] 17 notification: [android.app.INotificationManager] 18 mount: [IMountService] 19 throttle: [android.net.IThrottleManager] 20 connectivity: [android.net.IConnectivityManager] 21 wifi: [android.net.wifi.IWifiManager] 22 wifip2p: [android.net.wifi.p2p.IWifiP2pManager] 23 netpolicy: [android.net.INetworkPolicyManager] 24 netstats: [android.net.INetworkStatsService] 25 textservices: [com.android.internal.textservice.ITextServicesManager] 26 network_management: [android.os.INetworkManagementService] 27 clipboard: [android.content.IClipboard] 28 statusbar: [com.android.internal.statusbar.IStatusBarService] 29 device_policy: [android.app.admin.IDevicePolicyManager] 30 accessibility: [android.view.accessibility.IAccessibilityManager] 31 input_method: [com.android.internal.view.IInputMethodManager] 32 window: [android.view.IWindowManager] 33 alarm: [android.app.IAlarmManager] 34 vibrator: [android.os.IVibratorService] 35 battery: [] 36 hardware: [android.os.IHardwareService] 37 content: [android.content.IContentService] 38 account: [android.accounts.IAccountManager] 39 permission: [android.os.IPermissionController] 40 cpuinfo: [] 41 gfxinfo: [] 42 meminfo: [] 43 activity: [android.app.IActivityManager] 44 package: [android.content.pm.IPackageManager] 45 telephony.registry: [com.android.internal.telephony.ITelephonyRegistry] 46 usagestats: [com.android.internal.app.IUsageStats] 47 batteryinfo: [com.android.internal.app.IBatteryStats] 48 power: [android.os.IPowerManager] 49 entropy: [] 50 sensorservice: [android.gui.SensorServer] 51 media.audio_policy: [android.media.IAudioPolicyService] 52 media.camera: [android.hardware.IAudioPolicyServiceService] 53 media.player: [android.media.IMediaPlayerService] 54 media.audio_flinger: [android.media.IAudioFlinger] 55 SurfaceFlinger: [android.ui.ISurfaceComposer]
Wifi Service exposes WiFi functionality of the underlying system to the application layer via WifiManager class. The key differentiation between wifi stack and some other ones is that the wifi stack primarily uses the wpa_supplicant to talk to the Wifi driver.
Parsing and decoding of audio data by [Android_Media_Framework] (Stagefright) via the mediaplayer service
Writing decoded (PCM) data to an AudioTrack (AudioSink)
Mixing AudioTracks in AudioFlinger’s mixer thread (AudioFlinger::MixerThread::threadLoop())
Applying effects
Buffering stream data
Writing buffers to a PCM output device (e.g. ALSA driver) via android_audio_legacy.AudioHardware (audio legacy HAL) or "audio" hardware module HAL
/system/lib/audioflinger.so
/system/lib/libhardware.so
/system/lib/hw/audio.primary.tuna.so
/system/lib/libtinyalsa.so
/dev/snd/pcmC0D0p
The media framework are the APIs and libraries used for controlling playback and recording of video/audio. Since everything involving video and audio require a lot of computing power, mobile devices usually use a lot of special-purpose hardware for this, compared to desktop computers where there normally is enough raw power to run most/everything in software. Full-software solutions normally are more flexible and portable, but obviously require more processor power (which also might give a higher power consumption).
Normally only the highest level APIs are public, to allow for flexibility in the implementation internally.
java: android.media.MediaPlayer
frameworks/base/media/java/android/media/MediaPlayer.java
JNI: frameworks/base/media/jni/android_media_MediaPlayer.cpp
Quite straight mapping of java native functions to the C++ MediaPlayer class
C++: MediaPlayer
frameworks/base/media/libmedia/mediaplayer.cpp
IMediaPlayer, IMediaPlayerService
frameworks/base/include/media/IMediaPlayer.h
frameworks/base/include/media/IMediaPlayerService.h
BpMediaPlayer: Binder client/proxy interface
frameworks/base/media/libmedia/IMediaPlayer.cpp
BnMediaPlayer: Binder implementation interface (running in the media server)
BnMediaPlayer implemented by MediaPlayerService::Client
frameworks/base/libmediaplayservice/MediaPlayerService.cpp
MediaPlayerService backed by different implementations:
MediaPlayerService::AudioOutput
frameworks/base/libmediaplayservice/MediaPlayerService.cpp
AudioTrack
frameworks/base/media/libmedia/AudioTrack.cpp
IAudioTrack
AudioFlinger::TrackHandle
frameworks/base/services/audioflinger/AudioFlinger.cpp
The actual decoding/encoding in the OMXCodec and ACodec class is implemented via the IOMX interface:
The applications may also use OpenSL ES for playing back audio this internally uses OMXCodec from stagefright for decoding of the audio. (Code for this is in system/media/opensles/libopensles/android SfPlayer.cpp in gingerbread, in system/media/wilhelm/src/android/android AudioSfDecoder.cpp in ICS.) In ICS, there’s also a an OpenMAX AL API that can play back video, using the IMediaPlayerService interface.
Most of the APIs above in section 2 can either run in the calling application processes, or in the media server. Each of the Binder APIs (IAPI/BpAPI/BnAPI, such as IOMX, BpOMX, BnOMX) can proxy calls into the media server. The caller only gets an instance of the generic interface, e.g. IMediaPlayerm and does not know whether this is a proxy for calling the same interface in another process (BpOMX) or the actual implementation running within the same process (BnOMX).
Thanks to this, the whole concept of some parts of the API running within a separate process is mostly transparent when working with the APIs.
Stagefright is the library containing most of the implementation of the media framework.
Stagefright is a relatively recent development within android, replacing OpenCORE. It first appeared in Eclair, was made default player for all video (except for RTSP streaming which was still handled by OpenCORE) in Froyo, and in Gingerbread it had gained RTSP streaming support and was mature enough to be used for video recording, too, so OpenCORE was removed in Gingerbread. Since then, it has still evolved a bit.
Stagefright is mainly a generic pipeline kind of framework, similar to GStreamer, but much smaller and simpler. (It is purpose-written from scratch to fill the exact needs of Android, while OpenCORE was an already existing framework that PacketVideo provided at the time. Stagefright is a few orders of magnitude smaller than OpenCORE while still fulfilling all the needs Android has.)
Being a pipeline framework means that it consists of individual elements that form a pipeline, where each element either can produce data itself, or take input data from another element. By chaining such elements together, one can produce a pipeline that does the intended thing. E.g., for playing back video from a mp4 file on the phone, the following elements are hooked up together:
The AwesomePlayer creates the right MediaExtractor in finishSetDataSource_1, setting up the tracks in setDataSource l, and later creates the OMXCodec object for each of the tracks. The audio OMXCodec object is given as source object to AudioPlayer, which then reads data from it.
GStreamer has the same element/pipeline design, but is much more flexible supporting a number of modes of operation and a lot more elements, while Stagefright is simpler and smaller, since it only does exactly what is needed in Android.
The pipeline in Stagefright is pull-based, meaning that each element deriving from the base class MediaSource implements a method read(), which when called will block until the MediaSource element has produced one MediaBuffer which is returned. When the MediaSource subclass object is created, it is normally given an upstream MediaSource object to read from, e.g. for OMXCodec, the MediaSource is given as parameter to OMXCodec::Create().
For OMXCodec, the read() method internally reads a packet of compressed data from the source (which might e.g. be MPEG4Source within MPEG4Extractor in the case above), passes the packet to the actual OpenMAX codec for decoding, and when the decoded raw audio data is available, it is returned by the read() method.
Similarly, the MPEG4Source object will block in the read() method until it has read one packet from the source file, which is then returned to the caller. With this design, elements can be more or less arbitrarily connected, as long as one element can handle the data type that the input element produces.
For pipelines, Stagefright contains these kinds of element implementations:
In Honeycomb/Ice Cream Sandwich, Stagefright has evolved further, with a second mode of operation in addition to the plain pipeline based system. Now, the ACodec class encapsulates an OpenMAX codec with a different kind of abstraction. This class isn’t to be used as a strict pipeline element, thus, it doesn’t implement the MediaSource interface. Instead, messages are passed to ACodec (which implements AHandler, for receiving messages) with buffers to pass to the OpenMAX codec. This is more similar to the behavior model of the actual OpenMAX codecs themselves.
In addition to the elements, Stagefright contains higher level classes such as AwesomePlayer that coordinates playback of a video file, other utility classes such as color converters, wrapping of OpenMAX codecs (providing a Binder C++ interface around OpenMAX, giving it the possibility to use the codecs from a different process even if the codecs themselves run within the media server), and a number of software implementations of codecs.
|
|
FileSource, MPEG4Source and OMXCodec all implement the MediaSource interface. MPEG4Extractor, OMXCodec, AudioPlayer, MediaWriter all use the MediaSource interface for reading their input from the preceding element, of which they don’t need to know anything else than what’s in MediaSource. |
Stagefright provides wrapping around OMX core implementations. The main client api is called IOMX (frameworks/base/include/media/IOMX.h), which is a Binder interface with a proxy (BpOMX) and backend (BnOMX), allowing the caller and implementation to reside in separate processes. BnOMX is implemented by the OMX class in frameworks/base/media/libstagefright/omx/OMX.cpp, which uses the OMXMaster class in frameworks/base/media/libstagefright/omx/OMXMaster.cpp for querying and instantiating components from a number of different implementation sources. OMXMaster loads one or more plugins (OMXPluginBase, defined in frameworks/base/include/media/libstagefright/OMXPluginBase.h) that are analogous to normal OMX IL cores.
OMXMaster loads a shared library named libstagefrighthw.so, which should have a function named ZN7android15createOMXPluginEv(android::createOMXPlugin()) or createOMXPlugin (since ICS, both are tried, earlier, only the former was tried), that returns an OMXPluginBase pointer when called.
This library, libstagefrighthw.so, is vendor specific. Examples of implementations of this library, wrapping a normal OMX IL core, are available in:
All of these are very similar they simply load an OMX core from a shared library, load the OMX core function pointers from the library, and map the OMXPluginBase methods to the OMX core.
For software codecs, there’s no full proper OMX IL core in ICS, all decoders are mapped straight from the OMXPluginBase, implemented in frameworks/base/media/libstagefright/omx/SoftOMXPlugin.cpp. All software encoders (and in gingerbread, all software decoders too) are hooked up directly from the OMXCodec class, where the direct constructors of the software encoder classes are called, without any indirection via OMX like interfaces these software codecs implement the MediaSource interface directly. Due to this, these codecs cannot be accessed via the IOMX layer.
The development seems to be moving towards the OMX interfaces namely, in ICS, the software decoders have been converted to use OMX. The new ACodec class in ICS only uses codecs via the OMX interfaces.
Examples:
The OMX core acts as a registry for the available codecs. If the build configuration already contains a vendor specific OMX core, the new OMX components can either be added to this OMX core, or a new separate OMX core can be added.
Build libstagefright, containing an OMXPluginBase implementation that loads the OMX core. Examples:
If one chooses to add a second OMX core, a second libstagefrighthw (with a slightly different name) has to be created, and this also requires modifications to frameworks/base/media/libstagefright/omx/OMXMaster.cpp, in order to be able to handle two different vendor libraries at the same time. Therefore, it’s probably best to integrate new components within the current vendor OMX core if one already exists.
This is the actual wrapping of the codecs. Simple examples (which don’t use the proper real OMX API but only stagefright’s own internal API) are available in
frameworks/base/media/libstagefright/omx/SoftOMXComponent.cpp
frameworks/base/media/libstagefright/omx/SimpleSoftOMXComponent.cpp
frameworks/base/media/libstagefright/codecs/aacdec/SoftAAC.cpp
These contain all the logic any OMX component needs to have, but the external API isn’t the proper public OMX version, but only stagefright-internal ones. Real implementations with the proper public OMX API are available in e.g.:
Register the new OMX components in frameworks/base/media/libstagefright/OMXCodec.cpp. Simply add the component names and the mime types it handles in the kDecoderInfo/kEncoderInfo tables. (OMXCodec in stagefright doesn’t query for which components implement certain roles, but blindly checks for all the components listed to support a certain mime type, and uses the first one that actually exists.)
In this section, we’ll explore the inter-workings of the telephony stack. We’ll start with the standard Android Phone app, and trace the execution of placing a call all the way down to RIL daemon.
Android Framework provides the Telephony Manager class in android.telephony package. The Telephony Manager allows you to monitor the state of the mobile network connection. However, Telephony Manager does not allow you to place or manage any calls. Only the Phone app can initiate and answer phone calls.
|
|
Some of this content comes from the actual AOSP source code, licensed under Apache 2 License. |
Phone app does come with some UI components, such as the dialer, but the most significant part is its handling of CALL and CALL_PRIVILEGED intents to do the actual dialing of a number.
OutgoingCallBroadcaster activity receives CALL and CALL_PRIVILEGED Intents, and broadcasts the ACTION_NEW_OUTGOING_CALL intent which allows other applications to monitor, redirect, or prevent the outgoing call. After the other applications have had a chance to see the ACTION_NEW_OUTGOING_CALL intent, it finally reaches the OutgoingCallReceiver, which passes the (possibly modified) intent on to the SipCallOptionHandler, which will ultimately start the call using the CallController.placeCall() API.
CallController handler is the phone app module in charge of call control. This is a singleton object which acts as the interface to the telephony layer (and other parts of the Android framework) for all user-initiated telephony functionality, like making outgoing calls.
This functionality includes things like:
The single CallController instance stays around forever; it’s not tied to the lifecycle of any particular Activity (like the InCallScreen). There’s also no implementation of onscreen UI here (that’s all in InCallScreen).
Note that this class does not handle asynchronous events from the telephony layer, like reacting to an incoming call; see CallNotifier for that. This class purely handles actions initiated by the user, like outgoing calls.
placeCall(Intent intent) initiates an outgoing call.
Here’s the most typical outgoing call sequence:
OutgoingCallBroadcaster receives a CALL intent and sends the NEW_OUTGOING_CALL broadcast.
The broadcast finally reaches OutgoingCallReceiver, which stashes away a copy of the original CALL intent and launches SipCallOptionHandler.
SipCallOptionHandler decides whether this is a PSTN or SIP call (and in some cases brings up a dialog to let the user choose), and ultimately calls CallController.placeCall() (from the setResultAndFinish() method) with the stashed-away intent from step (2) as the "intent" parameter.
Here in CallController.placeCall() we read the phone number or SIP address out of the intent and actually initiate the call, and simultaneously launch the InCallScreen to display the in-call UI.
We handle various errors by directing the InCallScreen to display error messages or dialogs (via the InCallUiState "pending call status code" flag), and in some cases we also sometimes continue working in the background to resolve the problem (like in the case of an emergency call while in airplane mode). Any time that some onscreen indication to the user needs to change, we update the "status dialog" info in the inCallUiState and (re)launch the InCallScreen to make sure it’s visible.
PhoneUtils.placeCall(Context context, Phone phone, String number, Uri contactRef, boolean isEmergencyCall, Uri gatewayUri) dials the number using the phone passed in.
CallManager, defined in PhoneApp class.
CallManager class provides an abstract layer for PhoneApp to access and control calls. It implements Phone interface.
CallManager provides call and connection control as well as channel capability.
There are three categories of APIs CallManager provided
Call control and operation, such as dial() and hangup()
Channel capabilities, such as CanConference()
Register notification
public Connection dial(Phone phone, String dialString) in CallManager initiate a new voice connection. This happens asynchronously, so you cannot assume the audio path is connected (or a call index has been assigned) until PhoneStateChanged notification has occurred.
dial() uses PhoneProxy to implement Phone interface to make the call.
Phone interface specifies the capabilities of the underlying phone system, weather GSM, CDMA, or SIP. We’ll assume GMS from here on.
GSMPhone extends PhoneBase which in turn implements the Phone interface.
It uses GSMCallTracker to dial via CommandsInterface. This interface is implemented by RIL class.
RIL in turn uses RILRequest to send the requests to rild daemon.
|
|
You can use adb logcat -b radio to see the radio-specific log messages. |
RIL class is the implementation of the CommandsInterface that GSMPhone uses to dial out. This Java implementation writes out the commands to RIL daemon, the native code.
RIL uses RILSender and RILReceiver to send and receive messages from rild - the RIL Daemon. Unlike most of the other Android system services, RIL uses sockets for this inter-process communication, and not the Binder.
The RIL consists of two primary components:
|
|
Some of the following documentation comes from once available Android Platform Development Kit: Radio Layer Interface documentation. This documentation is no longer readily available from Google (deemed outdated) but is licensed under Apache 2.0 license. We’ve updated the references as originally provided. |
Android initializes the telephony stack and the Vendor RIL at startup as described in the sequence below:
RIL daemon reads rild.lib path and rild.libargs system properties to determine the Vendor RIL library to use and any initialization arguments to provide to the Vendor RIL.
RIL daemon loads the Vendor RIL library and calls RIL_Init to initialize the RIL and obtain a reference to RIL functions.
RIL daemon calls RIL_register on the Android telephony stack, providing a reference to the Vendor RIL functions
Details of this implementation are available in AOSP/hardware/ril/rild/rild.c.
There are two forms of communication that the RIL handles:
The following snippet illustrates the interface for solicited commands:
There are over sixty solicited commands grouped by the following families:
The following snippet illustrates the interface for unsolicited commands:
There are over ten unsolicited commands grouped by the following families:
To implement a radio-specific RIL, create a shared library that implements a set of functions required by Android to process radio requests. The required functions are defined in the RIL header (AOSP/hardware/ril/include/telephony/ril.h).
The Android radio interface is radio-agnostic and the Vendor RIL can use any protocol to communicate with the radio. Android provides a reference Vendor RIL, using the Hayes AT command set, that you can use as a quick start for telephony testing and a guide for commercial vendor RILs. The source code for the reference RIL is found at AOSP/hardware/ril/reference-ril.
Compile your Vendor RIL as a shared library using the convention libril-<companyname>-<RIL version>.so, for example, libril-acme-124.so, where:
For reference implementation of RIL, see AOSP/hardware/ril/libril.
It performs the measure and layout operation on its (invalidated) view hierarchy
It prepares to draw by asking its surface (which is just a client to the Surface Flinger) to dequeueBuffer,
The Surface Flinger asks Gralloc HAL for a buffer
The Gralloc HAL asks the kernel’s memory allocator to give it a block of physical memory (usually via PMEM, UMP, DMABUF, ION)
The Gralloc HAL provides a handle to this memory buffer back go Surface Flinger
The Surface Flinger sends this reference to the buffer as a file descriptor back to app via binder
The app gets the FD to the buffer
It does not explicitly try to map it or understand the memory - because this opaque memory blob is unique to the Gralloc implementation and the OpenGL drivers
It aggregates and optimizes all of its drawing commands into a display list
Issues the commands from the display list via the OpenGL API → driver into the buffer
Asks its surface (i.e. the Surface Flinger) to queueBuffer, which actually signals the Surface Flinger that the buffer is ready and that it should be posted to the screen at the next VSYNC
Figures out which buffers are visible on the screen as well as their relative z-order (with the help of WindowManagerService)
Composites (fuses) all the window buffers together using:
Sends the final image to the display via
See Exposing the Android Camera Stack by Balwinder Kaur and Joe Rickson from Aptina Imaging, Inc.
|
|
This service is deployed as an application, /system/app/Nfc.apk! |
Name at least five application framework services?
What is the most typical communication channel for a client to communicate with its service?
What’s the difference between managers and services?
How do we get the list of all application framework services?
What is the purpose of the servicemanager process?
How do clients/services get access to the servicemanager process?
What role does JNI play in most application framework services?
What is the life-cycle of most application framework services?
What is the relationship between application framework services and daemons?
How do most services communicate with their daemons?
What is the purpose of installd and who uses it and when?
What is the purpose of wpa_supplicant and who uses it?
Unlike the Vibrator (or Power) service, the Alarm service does not have what?
How do services (like Location service) provide call-backs to their clients?
What is AudioFlinger and what is its purpose?
What is the role of Audio Policy Service?
What is Stagefright and what is its purpose?
How do hardware manufactures take advantage of Stagefright?
How does the telephony stack differ from most other services?
What is SurfaceFlinger and what is its purpose?
How does NFC Service differ from most other services?
|
|
The following steps assume building Android JB 4.1.1 for ARMv7 on Ubuntu 10.04 x86_64 or MacOS X 10.7 and that the Android sources are available under a directory pointed to by $AOSP_HOME environment variable. (e.g. we could set AOSP_HOME=~/aosp/). |
|
|
The complete code (which we’ll develop in this section) for Marakana Alpha is available at https://github.com/marakana/alpha The code for Marakana Alpha SDK add-on is available https://github.com/marakana/alpha_sdk_addon |
While we could overlay our device’s custom components over the existing AOSP source tree, that makes it harder to deal with future OS upgrades. Instead, we will create a self-contained directory structure to host our device.
Go to AOSP directory
$ cd $AOSP_HOME
|
|
We assume that you set AOSP_HOME to the root of your Android Open Source Project source directory (e.g. ~/aosp/). Unless otherwise stated, the rest of the file paths are assumed to be relative to this directory. |
Create our vendor (e.g. mararkana) directory
$ mkdir device/marakana/
Now create our device (e.g. alpha) sub-directory:
$ mkdir device/marakana/alpha
We now want to add our device to the Android’s lunch list
|
|
Remember that $ source build/envsetup.sh registers lunch combos that we can later build |
Create vendorsetup.sh file for our device (the name of this file is fixed):
add_lunch_combo full_marakana_alpha-eng add_lunch_combo full_marakana_alpha-userdebug add_lunch_combo full_marakana_alpha-user
Re-build the lunch list:
$ source build/envsetup.sh including device/asus/grouper/vendorsetup.sh including device/generic/armv7-a-neon/vendorsetup.sh including device/generic/armv7-a/vendorsetup.sh including device/marakana/alpha/vendorsetup.sh including device/moto/wingray/vendorsetup.sh including device/samsung/crespo/vendorsetup.sh including device/samsung/maguro/vendorsetup.sh including device/ti/panda/vendorsetup.sh including sdk/bash_completion/adb.bash
Finally, we can check to see that our device now appear in the lunch menu:
$ lunch
You're building on Linux
Lunch menu... pick a combo:
1. full-eng
2. full_x86-eng
3. vbox_x86-eng
4. full_grouper-userdebug
5. mini_armv7a_neon-userdebug
6. mini_armv7a-userdebug
7. full_marakana_alpha-eng
8. full_marakana_alpha-userdebug
9. full_marakana_alpha-user
10. full_wingray-userdebug
11. full_crespo-userdebug
12. full_maguro-userdebug
13. full_maguro-eng
14. full_panda-userdebug
Which would you like? [full-eng]
$ lunch full_marakana_alpha-eng build/core/product_config.mk:203: *** No matches for product "full_marakana_alpha". Stop. ** Don't have a product spec for: 'full_marakana_alpha' ** Do you have the right repo manifest?
We now need to add basic support for building our device.
Start by creating a our own AndroidProducts.mk file, which simply defines the actual makefiles to be used when building our device:
PRODUCT_MAKEFILES := $(LOCAL_DIR)/full_alpha.mk
|
|
The only purpose of AndroidProducts.mk file (whose name is fixed) is to set PRODUCT_MAKEFILES to a list of product makefiles to expose to the build system. The only external variable it can use is LOCAL_DIR, whose value will be automatically set to the directory containing this file. |
Before we create the full_alpha.mk makefile, let’s create a common.mk where we’ll configure settings that will be shared between our Alpha device and our Alpha SDK Addon:
# Since this file can also be referenced by alpha-sdk_addon # we cannot assume LOCAL_PATH points to the directory where # this file is located. Instead, we create another variable # to capture this directory. MY_PATH := $(LOCAL_PATH)/../alpha # Include all makefiles in sub-directories (one level deep) include $(call all-subdir-makefiles)
Now we are ready to create the main build-file for our device (we call it full_alpha following the example from device/samsung/crespo/full_crespo.mk):
# Defines a list of languages to be supported by our device $(call inherit-product, $(SRC_TARGET_DIR)/product/languages_small.mk) # Defines the rules for building the base Android platform, # but itself is not specialized for any particular device $(call inherit-product, $(SRC_TARGET_DIR)/product/generic.mk) # Discard inherited values and use our own instead. PRODUCT_NAME := full_marakana_alpha PRODUCT_DEVICE := alpha PRODUCT_MODEL := Full Marakana Alpha Image for Emulator # Include the common definitions and packages include $(LOCAL_PATH)/common.mk
Our device inherits from the generic product:
$(call inherit-product, $(SRC_TARGET_DIR)/product/generic.mk)
which resolves to build/target/product/generic.mk file.
This generic product (file) in turn inherits from:
The combination of these files set up compilation rules that define which modules (a.k.a. PRODUCT_PACKAGES) get included in the final product (i.e. ROM).
Additionally, Android’s build system also defines GRANDFATHERED_USER_MODULES, which get included into every product, and are defined by build/core/user_tags.mk.
Next, we’ll import some boiler-plate make/support files from the "generic" board - since our device will run on the emulator:
$ cp build/target/board/generic/BoardConfig.mk device/marakana/alpha/. $ cp build/target/board/generic/AndroidBoard.mk device/marakana/alpha/. $ cp build/target/board/generic/device.mk device/marakana/alpha/. $ cp build/target/board/generic/system.prop device/marakana/alpha/.
|
|
For x86, copy from build/target/board/generic_x86/ instead. |
# config.mk # # Product-specific compile-time definitions. # # The generic product target doesn't have any hardware-specific pieces. TARGET_NO_BOOTLOADER := true TARGET_NO_KERNEL := true # Note: we build the platform images for ARMv7-A _without_ NEON. # # Technically, the emulator supports ARMv7-A _and_ NEON instructions, but # emulated NEON code paths typically ends up 2x slower than the normal C code # it is supposed to replace (unlike on real devices where it is 2x to 3x # faster). # # What this means is that the platform image will not use NEON code paths # that are slower to emulate. On the other hand, it is possible to emulate # application code generated with the NDK that uses NEON in the emulator. # TARGET_ARCH_VARIANT := armv7-a TARGET_CPU_ABI := armeabi-v7a TARGET_CPU_ABI2 := armeabi HAVE_HTC_AUDIO_DRIVER := true BOARD_USES_GENERIC_AUDIO := true # no hardware camera USE_CAMERA_STUB := true # Set /system/bin/sh to ash, not mksh, to make sure we can switch back. TARGET_SHELL := ash # Enable dex-preoptimization to speed up the first boot sequence # of an SDK AVD. Note that this operation only works on Linux for now ifeq ($(HOST_OS),linux) WITH_DEXPREOPT := true endif # Build OpenGLES emulation guest and host libraries BUILD_EMULATOR_OPENGL := true # Build and enable the OpenGL ES View renderer. When running on the emulator, # the GLES renderer disables itself if host GL acceleration isn't available. USE_OPENGL_RENDERER := true
Because we copied build/target/board/generic/BoardConfig.mk we inherited the following:
TARGET_ARCH_VARIANT := armv7-a TARGET_CPU_ABI := armeabi-v7a TARGET_CPU_ABI2 := armeabi
Alternatively, we could have built our image for x86, either by copying build/target/board/generic_x86/BoardConfig.mk or by changing our own device/marakana/alpha/BoardConfig.mk to say the following:
TARGET_ARCH := x86 TARGET_ARCH_VARIANT := x86-atom TARGET_CPU_ABI := x86
Finally, let’s change the default wallpaper for our device (for fun) by taking advantage of the build system’s overlay feature:
|
|
The overlay feature enables us to override specific files in the Android framework/app res/ folders, without having to change anything in the AOSP source directories. This is usually used to customize the look and feel as well as app/system-specific configuration settings. In our case, we want to replace frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.jpg file in the final image because that’s what the Launcher2 defaults to. |
Make the specific overlay/ directory:
$ mkdir -p device/marakana/alpha/overlay/frameworks/base/core/res/res/drawable-nodpi
Push our default_wallpaper.jpg to it:
|
|
This image is also available on GitHub |
Finally, we need to tell the build-system to use our overlay/ directory:
… # Enable overlays DEVICE_PACKAGE_OVERLAYS := $(MY_PATH)/overlay
Before we compile our device, we should generate our own platform signing keys [Platform_Keys], otherwise our device will not pass CTS.
Define our subject/issuer info:
$ SIGNER="/C=US/ST=California/L=San Francisco/O=Marakana Inc./OU=Android/CN=Android Platform Signer/emailAddress=android@marakana.com"
Remove the existing keys (it does not hurt to back them up first!):
$ rm build/target/product/security/*.p*
Generate the platform key:
$ echo | development/tools/make_key build/target/product/security/platform "$SIGNER" creating build/target/product/security/platform.pk8 with no password Generating RSA private key, 2048 bit long modulus ....................+++ ..........................................................+++ e is 3 (0x3)
Generate the shared key:
$ echo | development/tools/make_key build/target/product/security/shared "$SIGNER" creating build/target/product/security/shared.pk8 with no password Generating RSA private key, 2048 bit long modulus ..................................................................................................+++ ............+++ e is 3 (0x3)
Generate the media key:
$ echo | development/tools/make_key build/target/product/security/media "$SIGNER" creating build/target/product/security/media.pk8 with no password Generating RSA private key, 2048 bit long modulus ...................+++ ....................+++ e is 3 (0x3)
Generate the testkey key:
$ echo | development/tools/make_key build/target/product/security/testkey "$SIGNER" creating build/target/product/security/testkey.pk8 with no password Generating RSA private key, 2048 bit long modulus ....................+++ ................................................+++ e is 3 (0x3)
Verify that our keys have been created:
$ ls -1 build/target/product/security/*.p* build/target/product/security/media.pk8 build/target/product/security/media.x509.pem build/target/product/security/platform.pk8 build/target/product/security/platform.x509.pem build/target/product/security/shared.pk8 build/target/product/security/shared.x509.pem build/target/product/security/testkey.pk8 build/target/product/security/testkey.x509.pem
Check that our specific subject/issuer has been used:
$ openssl x509 -noout -subject -issuer -in build/target/product/security/platform.x509.pem subject= /C=US/ST=California/L=San Francisco/O=Marakana Inc./OU=Android/CN=Android Platform Signer/emailAddress=android@marakana.com issuer= /C=US/ST=California/L=San Francisco/O=Marakana Inc./OU=Android/CN=Android Platform Signer/emailAddress=android@marakana.com
|
|
The build/target/product/security/.pk8 files are the private keys (.x509.pem are the certificates), and we need to make sure to keep them safe and secure - especially since we did not encrypt them! |
We are now ready to try out our "custom" device - though there is nothing truly custom yet, except for the PRODUCT_* settings and our own product keys.
For good measure, re-register our device:
$ source build/envsetup.sh including device/marakana/alpha/vendorsetup.sh …
Now we can lunch of our device:
$ lunch full_marakana_alpha-eng ============================================ PLATFORM_VERSION_CODENAME=REL PLATFORM_VERSION=4.0.3 TARGET_PRODUCT=full_marakana_alpha TARGET_BUILD_VARIANT=eng TARGET_BUILD_TYPE=release TARGET_BUILD_APPS= TARGET_ARCH=arm TARGET_ARCH_VARIANT=armv7-a HOST_ARCH=x86 HOST_OS=linux HOST_BUILD_TYPE=release BUILD_ID=IML74K ============================================
We can now compile our device:
$ make -j10 … Installed file list: out/target/product/alpha/installed-files.txt Target system fs image: out/target/product/alpha/obj/PACKAGING/systemimage_intermediates/system.img Install system fs image: out/target/product/alpha/system.img
|
|
Adjust the value of -j based on your system’s available cores, memory, and disk I/O. |
Finally, we can run it
$ $ANDROID_HOST_OUT/bin/emulator &
And we should see
Our device would work fine with the provided QEMU-based (i.e. emulator-specific) kernel, but in this section, we will configure a custom kernel ([Android_Building_Linux_Kernel]) so that we can:
Pre-build QEMU kernels are available in AOSP source tree:
The steps outlined here are similar to [Android_Building_Linux_Kernel_for_Emulator]:
Create a directory to host our kernel sources:
$ mkdir ~/kernel/ $ cd ~/kernel/
Clone the Goldfish kernel sources:
$ git clone https://android.googlesource.com/kernel/goldfish.git $ cd goldfish/
Check out android-goldfish-2.6.29 branch of the kernel we wish to build:
$ git checkout -t remotes/origin/android-goldfish-2.6.29
Set the architecture variable to match TARGET_ARCH_VARIANT we set in device/marakana/alpha/BoardConfig.mk:
$ export ARCH=arm
|
|
When compiling for x86, set the following instead: $ export ARCH=x86 |
Create the default kernel configuration file (.config):
$ make goldfish_armv7_defconfig
|
|
When compiling for ARMv5 or x86, run the following instead: $ make goldfish_defconfig |
Change the Hardware name from "Goldfish" to "Marakana Alpha Board":
… MACHINE_START(GOLDFISH, "Marakana Alpha Board") …
As we already know, when the Android kernel loads, it runs init, which configures itself from init.rc and init.hardware_.rc configuration files.
The hardware name is extracted from /proc/cpuinfo:
$ adb shell cat /proc/cpuinfo |grep Hardware Hardware : Goldfish
Basically, system/core/init/init.c:main() uses system/core/init/util.c:get_hardware_name(…) to parse /proc/cpuinfo and extract the hardware name. This name is converted to lower-case and all the space characters are trimmed.
Android’s ueventd process (started by init) also configures itself in a similar way, by loading ueventd.rc and ueventd.hardware.rc configuration files.
Since in our case we changed the name of our hardware to "Marakana Alpha Board", we will also need to create the corresponding: init.marakanaalphaboard.rc and ueventd.marakanaalphaboard.rc files.
Alternatively, the hardware name may also be set by the bootloader via kernel boot options. These are exposed to the userspace via /proc/cmdline (e.g. … androidboot.hardware=goldfish …)
Configure the kernel
$ make menuconfig
Set General setup → Local version to -marakana-alpha-release
Select Enable loadable module support and within it also select Module unloading and within that Forced module unloading
Customize the rest as desired
Set the cross compiler:
$ export CROSS_COMPILE=$ANDROID_EABI_TOOLCHAIN/arm-linux-androideabi-
|
|
When compiling for x86, set the following instead: $ export REAL_CROSS_COMPILE=$ANDROID_EABI_TOOLCHAIN/i686-android-linux- $ export CROSS_COMPILE=$AOSP/external/qemu/distrib/kernel-toolchain/android-kernel-toolchain- Android’s x86 compiler is NDK-compatible, so it enforces -mfpmath=sse and -fpic by default. When building the kernel, we need to disable this. In this case, $CROSS_COMPILE points to a toolchain of shell scripts, which add -mfpmath=387 -fno-pic flags before calling into $REAL_CROSS_COMPILE toolchain. |
The environment variable ANDROID_EABI_TOOLCHAIN was initialized by lunch when we ran lunch full_marakana_alpha-eng.
Its value varies based on the version of Android, host architecture, and target architecture:
Compile the kernel
$ make -j4
|
|
We’ve had issues compiling the kernel on Mac OS X Lion (10.7). |
AOSP comes with a convenient script to compile the Linux kernel for QEMU-based emulator: $AOSP_HOME/external/qemu/distrib/build-kernel.sh.
We could use this script instead of setting up our own ARCH and CROSS_COMPILE options and running make.
For example, to compile for x86, we could do:
$ $AOSP_HOME/external/qemu/distrib/build-kernel.sh --arch=x86
The target kernel will go to /tmp/kernel-qemu/ by default.
Copy the compiled kernel to our device’s alpha/ directory:
$ cp arch/arm/boot/zImage $AOSP_HOME/device/marakana/alpha/kernel
|
|
For x86, copy arch/x86/boot/bzImage instead. |
|
|
In case you were not able to compile the kernel, you could also download a pre-built one (for ARMv7) from https://github.com/marakana/alpha/raw/master/kernel |
Now we can go back to the AOSP source
$ cd $AOSP_HOME/
Enable our custom kernel in BoardConfig.mk (double-negation, nice!):
… TARGET_NO_KERNEL := false …
|
|
This will cause the build system to rebuild the boot.img file, but because we are still working with the emulator, we’ll have to explicitly reference our kernel on startup. |
Now we are ready to create our own init and ueventd configuration files by copying the ones from Goldfish:
$ cp system/core/rootdir/etc/init.goldfish.rc device/marakana/alpha/init.marakanaalphaboard.rc $ cp system/core/rootdir/etc/ueventd.goldfish.rc device/marakana/alpha/ueventd.marakanaalphaboard.rc
|
|
Remember, marakanaalphaboard is our device’s new hardware name (lowercased and stripped of spaces). |
Next, we need to enable our kernel and copy the init and ueventd configuration files:
… # Enable our custom kernel LOCAL_KERNEL := $(MY_PATH)/kernel PRODUCT_COPY_FILES += $(LOCAL_KERNEL):kernel # Copy our init and ueventd configuration files to the root # file system (ramdisk.img -> boot.img) PRODUCT_COPY_FILES += $(MY_PATH)/init.marakanaalphaboard.rc:root/init.marakanaalphaboard.rc PRODUCT_COPY_FILES += $(MY_PATH)/ueventd.marakanaalphaboard.rc:root/ueventd.marakanaalphaboard.rc
Re-build our ROM, which, in this case just creates a new out/target/product/alpha/boot.img (that we won’t actually use):
$ make -j10 …
Restart the emulator (with our new kernel)
$ $ANDROID_HOST_OUT/bin/emulator -kernel out/target/product/alpha/kernel &
|
|
We have to run our emulator with -kernel, because otherwise it would ignore the kernel in our newly built boot.img and instead use the default one (prebuilt/android-arm/kernel/kernel-qemu-armv7). This applies just to the emulator. |
Test
$ adb shell cat /proc/version Linux version 2.6.29-marakana-alpha-release-g46b05b2 (sasa@thermal) (gcc version 4.4.3 (GCC) ) #3 Tue Jan 3 13:25:44 PST 2012
|
|
The path to adb was added to our PATH when we $ source build/envsetup.sh (on a Linux host, it comes from out/host/linux-x86/bin/adb) |
We could also take a look at the updated About screen:
We start by creating a home for our header files:
$ mkdir device/marakana/alpha/include/
Next, we can create a simple header file (mrknlog.h) for our library:
#ifndef _MRKNLOG_INTERFACE_H #define _MRKNLOG_INTERFACE_H #include <stdint.h> #include <sys/cdefs.h> #include <sys/types.h> #include <hardware/hardware.h> __BEGIN_DECLS #define MRKNLOG_HARDWARE_MODULE_ID "mrknlog" struct mrknlog_device_t { struct hw_device_t common; int fd; /* * Flush the log device * * Returns: 0 on success, error code on failure */ int (*flush_log)(struct mrknlog_device_t* dev); /* * Get the total log size * * Returns: total log size, < 0 on failure */ int (*get_total_log_size)(struct mrknlog_device_t* dev); /* * Get the used log size * * Returns: used log size, < 0 on failure */ int (*get_used_log_size)(struct mrknlog_device_t* dev); /* * Wait until more log data becomes available or until timeout expires * timeout: the max wait time (in ms). The value of -1 means wait forever * Returns: < 0 or error, 0 on timeout, the amount of new data (in bytes) */ int (*wait_for_log_data)(struct mrknlog_device_t* dev, int timeout); }; __END_DECLS #endif /* End of the _MRKNLOG_INTERFACE_H block */
While we could’ve implemented our HAL as a simple shared library, we chose to build it as a libhardware.so-compliant module, by following the rules set by hardware/libhardware/include/hardware/hardware.h.
Our module (mrknlog_device_t) is actually a custom data structure where we:
Who says we cannot do OOP in C? :-)
Next, we create a home for our shared libraries:
$ mkdir device/marakana/alpha/lib/
Since our actual libraries will be in their own sub-directories, we need to include them in our build system, via a simple makefile:
include $(call all-subdir-makefiles)
Now we can create an actual directory for our libmrknlog HAL module library:
$ mkdir device/marakana/alpha/lib/libmrknlog
Finally, we are ready to implement our shared library (libmrknlog.c) - using ioctl(…), poll(…), and read(…) to talk to the kernel driver:
#define LOG_NDEBUG 1 /**/ #define LOG_FILE "/dev/log/main" /*
*/ #define LOG_TAG "MrknLog" /*
*/ #include <mrknlog.h> /*
*/ #include <cutils/log.h> /*
*/ #include <cutils/logger.h> /*
*/ #include <fcntl.h> #include <poll.h> /*
*/ #include <errno.h> #include <sys/ioctl.h> /*
*/ static int ioctl_log(int mode, int request) { int logfd = open(LOG_FILE, mode); /*
*/ if (logfd < 0) { SLOGE("Failed to open %s: %s", LOG_FILE, strerror(errno)); /*
*/ return -1; } else { int ret = ioctl(logfd, request); /*
*/ close(logfd); /*
*/ return ret; } } static int flush_log(struct mrknlog_device_t* dev) { SLOGV("Flushing %s", LOG_FILE); /*
*/ return ioctl_log(O_WRONLY, LOGGER_FLUSH_LOG); /*
*/ } static int get_total_log_size(struct mrknlog_device_t* dev) { SLOGV("Getting total buffer size of %s", LOG_FILE); /*
*/ return ioctl_log(O_RDONLY, LOGGER_GET_LOG_BUF_SIZE); /*
*/ } static int get_used_log_size(struct mrknlog_device_t* dev) { SLOGV("Getting used buffer size of %s", LOG_FILE); /*
*/ return ioctl_log(O_RDONLY, LOGGER_GET_LOG_LEN); /*
*/ } static int wait_for_log_data(struct mrknlog_device_t* dev, int timeout) { SLOGV("Waiting for log data on %s with timeout=%d", LOG_FILE, timeout); /*
*/ int ret; struct pollfd pfd; pfd.fd = dev->fd; pfd.events = POLLIN; ret = poll(&pfd, 1, timeout); /*
*/ if (ret < 0) { SLOGE("Failed to poll %s: %s", LOG_FILE, strerror(errno)); /*
*/ return -1; } else if (ret == 0) { return 0; /* timeout */ } else { /* consume all of the available data */ unsigned char buf[LOGGER_ENTRY_MAX_LEN + 1] __attribute__((aligned(4))); int new_data_counter = 0; while(1) { /* we have to read because the file is not seekable */ ret = read(dev->fd, buf, LOGGER_ENTRY_MAX_LEN); /*
*/ if (ret < 0) { SLOGE("Failed to read %s: %s", LOG_FILE, strerror(errno)); /*
*/ return -1; } else if (ret == 0) { SLOGE("Unexpected EOF on reading %s", LOG_FILE); /*
*/ return -1; } else { new_data_counter += ret; /* check to see if there is more data to read */ ret = ioctl(dev->fd, LOGGER_GET_NEXT_ENTRY_LEN); /*
*/ if (ret < 0) { SLOGE("Failed to get next entry len on %s: %s", /*
*/ LOG_FILE, strerror(errno)); return -1; } else if (ret == 0) { /* no more data; we are done */ SLOGV("Got %d / %d / %d on %s", new_data_counter, /*
*/ get_used_log_size(dev), get_total_log_size(dev), LOG_FILE); return new_data_counter; } } } } } static int close_mrknlog(struct mrknlog_device_t* dev) { SLOGV("Closing %s", LOG_FILE); if (dev) { close(dev->fd); /*
*/ free(dev); /*
*/ } return 0; } static int open_mrknlog(const struct hw_module_t *module, char const *name, struct hw_device_t **device) { int fd = open(LOG_FILE, O_RDWR); /*
*/ if (fd < 0) { SLOGE("Failed to open %s: %s", LOG_FILE, strerror(errno)); return -1; } else { struct mrknlog_device_t *dev = /*
*/ malloc(sizeof(struct mrknlog_device_t)); /*
*/ if (!dev) { return -ENOMEM; } SLOGV("Opened %s", LOG_FILE); /*
*/ memset(dev, 0, sizeof(*dev)); /*
*/ dev->common.tag = HARDWARE_DEVICE_TAG; /*
*/ dev->common.version = 0; /*
*/ dev->common.module = (struct hw_module_t *)module; /*
*/ dev->common.close = /*
*/ (int (*)(struct hw_device_t *)) close_mrknlog; dev->fd = fd; /*
*/ dev->flush_log = flush_log; /*
*/ dev->get_total_log_size = get_total_log_size; /*
*/ dev->get_used_log_size = get_used_log_size; /*
*/ dev->wait_for_log_data = wait_for_log_data; /*
*/ *device = (struct hw_device_t *)dev; return 0; } } static struct hw_module_methods_t mrknlog_module_methods = { .open = open_mrknlog, /*
*/ }; struct hw_module_t HAL_MODULE_INFO_SYM = { /*
*/ .tag = HARDWARE_MODULE_TAG, .version_major = 1, .version_minor = 0, .id = MRKNLOG_HARDWARE_MODULE_ID, /*
*/ .name = "mrknlog module", .author = "Marakana, Inc.", .methods = &mrknlog_module_methods, /*
*/ };
| Here we are using SLOG<X> macros, defined by system/core/include/cutils/log.h, which use liblog.so to log to the system log buffer. The log tag for each statement is defined by LOG_TAG. To enable verbose logging, set LOG_NDEBUG to 0. The value of 1 renders all SLOGV as no-ops. | |
| This is the driver we are "wrapping" with our HAL. It’s the main log buffer. | |
| These definitions come from <mrknlog.h> we previously created. | |
| All of the ioctl commands are defined by system/core/include/cutils/logger.h. | |
| These are our actual interactions with the driver. To see how these are implemented on this other side, take a look at drivers/staging/android/logger.c in the kernel source directory. | |
| This is where we manage the state of our module. | |
| Here we initialize our "methods", including the two that are responsible for opening and closing or module. | |
| This is the entry into our module, as required by libhardware. This is very similar to how kernel drivers are structured. |
We are now ready for the makefile (Android.mk):
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../include/ LOCAL_SRC_FILES := libmrknlog.c LOCAL_SHARED_LIBRARIES := libcutils LOCAL_MODULE := mrknlog.default LOCAL_MODULE_PATH := $(TARGET_OUT_SHARED_LIBRARIES)/hw LOCAL_CFLAGS += -g -O0 include $(BUILD_SHARED_LIBRARY)
|
|
Here, we use:
|
In previous versions of Android (pre 4.0), we would’ve also needed to add LOCAL_PRELINK_MODULE := false in order to prevent pre-linking of our library. Pre-linked libraries were registered to be loaded at fixed memory addresses in order to speed up linking and allow for faster booting (at the expense of ASLR). If we did not disable it, we would’ve had to register our library build/core/prelink-linux-arm.map (now deprecated).
Optionally, we can compile our library to test that it builds:
$ make mrknlog.default … target thumb C: mrknlog.default <= device/marakana/alpha/lib/libmrknlog/libmrknlog.c target SharedLib: mrknlog.default (out/target/product/alpha/obj/SHARED_LIBRARIES/mrknlog.default_intermediates/LINKED/mrknlog.default.so) target Symbolic: mrknlog.default (out/target/product/alpha/symbols/system/lib/hw/mrknlog.default.so) Export includes file: device/marakana/alpha/lib/libmrknlog/Android.mk -- out/target/product/alpha/obj/SHARED_LIBRARIES/mrknlog.default_intermediates/export_includes target Strip: mrknlog.default (out/target/product/alpha/obj/lib/mrknlog.default.so) Install: out/target/product/alpha/system/lib/hw/mrknlog.default.so
Android’s build system allows us to compile individual modules via a simple $ make <module-name> where <module-name> is defined by LOCAL_MODULE (or LOCAL_PACKAGE_NAME for apps) in the module’s Android.mk file. To clean just this one module, we can do $ make clean-<module-name>.
This works, but it still takes a long time (about 2mins 13.3s on a 2.3GHz i7 with 16GB of RAM and a hybrid SDD/HDD) because the build system has to scan the entire source tree to find our module.
Alternatively, we can use the mm function right from the module’s directory (takes about 6.5s):
$ godir libmrknlog device/marakana/alpha/lib/libmrknlog$ mm … device/marakana/alpha/lib/libmrknlog$ croot $
The function godir builds a filelist index of all of the source-files, searches for the file that matches the argument name in that list, and then jumps to the directory containing that file. In this case, it’s equivalent to cd device/marakana/alpha/lib/libmrknlog command. The function croot goes to the root of the AOSP source tree. In this case, it’s equivalent to cd -.
Another way to compile just a particular module (by its directory path) is to use the mmm function from the root of the source tree:
$ mmm device/marakana/alpha/lib/libmrknlog …
Bash functions godir, croot, mm, and mmm are defined by build/envsetup.sh, which we sourced into our shell.
Since our library’s Android.mk states LOCAL_MODULE_TAGS := optional, we need to register it with Alpha’s PRODUCT_PACKAGES, otherwise it will not be included in the final ROM:
Create a new packages.mk makefile just for registering packages (making it easier to use it by other devices):
PRODUCT_PACKAGES += mrknlog.defaultInclude this file into our common.mk we previously created:
… # Include all packages from this file include $(MY_PATH)/packages.mk
Before we can create our executable to test our library, let’s create a directory for all binaries:
$ mkdir device/marakana/alpha/bin
Like with our libraries, we need to "recursively" include makefiles in sub-directories of …/bin/, which is where our executables will live:
include $(call all-subdir-makefiles)
Now we can create a directory for our mrknlog executable:
$ mkdir device/marakana/alpha/bin/mrknlog
Next we provide the implementation (mrknlog.c), which will use our shared library (mrknlog.default.so):
#include <stdio.h> #include <string.h> #include <errno.h> #include <mrknlog.h> #include <hardware/hardware.h> int main (int argc, char* argv[]) { hw_module_t* module; int ret = hw_get_module(MRKNLOG_HARDWARE_MODULE_ID, (hw_module_t const**)&module); if (ret == 0) { struct mrknlog_device_t *dev; ret = module->methods->open(module, 0, (struct hw_device_t **) &dev); if (ret == 0) { int usedSize = dev->get_used_log_size(dev); int totalSize = dev->get_total_log_size(dev); if (totalSize >= 0 && usedSize >= 0) { if (dev->flush_log(dev) == 0) { printf("Flushed log. Previously it was consuming %d of %d bytes\n", usedSize, totalSize); ret = 0; } else { fprintf(stderr, "Failed to flush log: %s", strerror(errno)); ret = -1; } } else { fprintf(stderr, "Failed to get log size: %s", strerror(errno)); ret = -2; } dev->common.close((struct hw_device_t *)dev); } else { fprintf(stderr, "Failed to open device: %d", ret); ret = -3; } } else { fprintf(stderr, "Failed to get module: %s", MRKNLOG_HARDWARE_MODULE_ID); ret = -4; } return ret; }
The pattern for using hardware/hardware.h-compatible HAL modules is pretty standard:
Define a local pointer to the module to load
hw_module_t* module;
Load the module by its ID (usually defined in the modules’s header file)
hw_get_module(FOO_HARDWARE_MODULE_ID, (hw_module_t const**)&module);
Use the module’s standard open "method" to initialize the actual module
struct foo_device_t *dev; module->methods->open(module, 0, (struct hw_device_t **) &dev);
"Invoke" module’s "methods" (by passing the module to each function you call):
dev->do_some_foo_operation(dev, …);
When done, close the module cleanly:
dev->common.close((struct hw_device_t *)dev);
As with everything else, we need a makefile (Android.mk) to build our executable:
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := mrknlog.c LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../include/ LOCAL_SHARED_LIBRARIES := libhardware LOCAL_CFLAGS += -g -O0 LOCAL_MODULE := mrknlog include $(BUILD_EXECUTABLE)
|
|
Here, we need to tell the compiler where to find our library’s header file: LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../include/ and tell the linker to link against libhardware (which will in turn load mrknlog.default.so): LOCAL_SHARED_LIBRARIES := libhardware Finally, unlike last time when we include $(BUILD_SHARED_LIBRARY), this time we: include $(BUILD_EXECUTABLE) which will produce system/bin/mrknlog |
We can compile our executable to test that it builds:
$ rm filelist $ godir mrknlog.c [1] ./device/marakana/alpha/bin/mrknlog [2] ./device/marakana/alpha/lib/libmrknlog Select one: 1 device/marakana/alpha/bin/mrknlog$ mm … target thumb C: mrknlog <= device/marakana/alpha/bin/mrknlog/mrknlog.c target Executable: mrknlog (out/target/product/alpha/obj/EXECUTABLES/mrknlog_intermediates/LINKED/mrknlog) target Symbolic: mrknlog (out/target/product/alpha/symbols/system/bin/mrknlog) Export includes file: device/marakana/alpha/bin/mrknlog/Android.mk -- out/target/product/alpha/obj/EXECUTABLES/mrknlog_intermediates/export_includes target Strip: mrknlog (out/target/product/alpha/obj/EXECUTABLES/mrknlog_intermediates/mrknlog) Install: out/target/product/alpha/system/bin/mrknlog … device/marakana/alpha/bin/mrknlog$ croot $
Just like with our library, since our executable’s Android.mk states LOCAL_MODULE_TAGS := optional, we need to register it with Alpha’s PRODUCT_PACKAGES, or otherwise it will not be included in the final ROM:
…
PRODUCT_PACKAGES += mrknlogNow we can rebuild our entire device:
$ make -j10 … Install system fs image: out/target/product/alpha/system.img
Finally, we are ready to test our library/executable:
# (re)start the emulator $ $ANDROID_HOST_OUT/bin/emulator -kernel out/target/product/alpha/kernel & # wait for the emulator to finish # check out our library $ adb shell ls -l /system/lib/hw/mrknlog.default.so -rw-r--r-- root root 5460 2012-01-04 10:02 mrknlog.default.so # check out our utility $ adb shell ls -l /system/bin/mrknlog -rwxr-xr-x root shell 5612 2012-01-04 10:20 mrknlog # check if our utility is doing what it is supposed to $ adb logcat -g /dev/log/main: ring buffer is 64Kb (54Kb consumed), max entry is 4096b, max payload is 4076b $ adb shell /system/bin/mrknlog Flushed log. Previously it was consuming 55981 of 65536 bytes $ adb logcat -g /dev/log/main: ring buffer is 64Kb (0Kb consumed), max entry is 4096b, max payload is 4076b # check again $ adb shell /system/bin/mrknlog Flushed log. Previously it was consuming 0 of 65536 bytes # good :-)
As before, we need to create a home for our daemon source:
$ mkdir device/marakana/alpha/bin/mrknlogd
Next, we create or daemon source (mrknlogd.c), by utilizing our libmrknlog library:
#define LOG_TAG "MRKN Log Daemon" #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> #include <mrknlog.h> #include <cutils/log.h> #include <hardware/hardware.h> int runFlag = 1; static void sig_handler(int signo) { SLOGI("Caught signal %d. Scheduling exit.", signo); runFlag = 0; } int main (int argc, char* argv[]) { int ret; if (argc != 2) { fprintf(stderr, "Usage: %s <flush-frequency-in-seconds>\n", argv[0]); ret = -1; } else { hw_module_t* module; ret = hw_get_module(MRKNLOG_HARDWARE_MODULE_ID, (hw_module_t const**)&module); if (ret == 0) { struct mrknlog_device_t *dev; ret = module->methods->open(module, 0, (struct hw_device_t **) &dev); if (ret == 0) { int frequency = atoi(argv[1]); int totalSize = dev->get_total_log_size(dev); int usedSize; int count = 1; while(runFlag) { usedSize = dev->get_used_log_size(dev); if (dev->flush_log(dev) == 0) { SLOGI("Flushed log (%d, %d of %d bytes). Waiting %d seconds before the next flush.", count, usedSize, totalSize, frequency); } else { SLOGE("Failed to flush log. Bailing out"); break; } count++; sleep(frequency); } SLOGI("Done after %d iterations.", count); dev->common.close((struct hw_device_t *)dev); } else { fprintf(stderr, "Failed to open device: %d", ret); ret = -2; } } else { fprintf(stderr, "Failed to get module: %s", MRKNLOG_HARDWARE_MODULE_ID); ret = -3; } } return ret; }
|
|
This program is designed to run as a "daemon" simply by running infinitely in a while(1) {…} block. |
|
|
We can use SLOGI(), SLOGE(), and other such macros, which simplify logging: #define SLOGI(…) void)android_log_buf_print(LOG_ID_SYSTEM #define SLOGE(…) void)android_log_buf_print(LOG_ID_SYSTEM |
Next, we create our makefile (Android.mk), which defines links from our source to our library:
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := mrknlogd.c LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../include/ LOCAL_SHARED_LIBRARIES := libcutils libhardware LOCAL_CFLAGS += -g -O0 LOCAL_MODULE := mrknlogd include $(BUILD_EXECUTABLE)
Now we can compile our daemon to test that it builds:
$ mmm device/marakana/alpha/bin/mrknlogd/ … target thumb C: mrknlogd <= device/marakana/alpha/bin/mrknlogd/mrknlogd.c target Executable: mrknlogd (out/target/product/alpha/obj/EXECUTABLES/mrknlogd_intermediates/LINKED/mrknlogd) target Symbolic: mrknlogd (out/target/product/alpha/symbols/system/bin/mrknlogd) Export includes file: device/marakana/alpha/bin/mrknlogd/Android.mk -- out/target/product/alpha/obj/EXECUTABLES/mrknlogd_intermediates/export_includes target Strip: mrknlogd (out/target/product/alpha/obj/EXECUTABLES/mrknlogd_intermediates/mrknlogd) Install: out/target/product/alpha/system/bin/mrknlogd
Just like with our previous executable/library, we need to register our daemon with Alpha’s PRODUCT_PACKAGES, or otherwise it will not be included in the final ROM:
…
PRODUCT_PACKAGES += mrknlogdBefore we can rebuild our ROM, we need to configure init to start our mrknlogd daemon as a service, and to do that, we need to register this in our init.marakanaalphaboard.rc file:
…
on boot
…
start mrknlogd
…
# Marakana's custom log-flushing daemon
service mrknlogd /system/bin/mrknlogd 60
user system
group log
oneshot
|
|
As opposed to start mrknlogd in on boot, we could’ve simply assigned class main to our mrknlogd service. |
In order to get the size of the log buffer /dev/log/main, we need to be able to read from this "file", and for that, we need to be either root or belong to group log (we chose the latter):
$ adb shell ls -l /dev/log/main crw-rw--w- root log 10, 57 2012-01-05 00:02 main
Now we can rebuild our entire device:
$ make -j10 … build/core/Makefile:25: warning: overriding commands for target ++out/target/product/alpha/root/init.goldfish.rc' system/core/rootdir/Android.mk:58: warning: ignoring old commands for target ++out/target/product/alpha/root/init.goldfish.rc' … Install system fs image: out/target/product/alpha/system.img Installed file list: out/target/product/alpha/installed-files.txt
|
|
There warnings are (unfortunately) expected because our rule to copy init.goldfish.rc (implemented by build/core/Makefile) conflicts with what Android wants to do already (in system/core/rootdir/Android.mk). |
Finally, we are ready to test our daemon:
# (re)start the emulator $ $ANDROID_HOST_OUT/bin/emulator -kernel out/target/product/alpha/kernel & # wait for the emulator to finish # check out our daemon $ adb shell ls -l /system/bin/mrknlogd -rwxr-xr-x root shell 5636 2012-01-04 20:52 mrknlogd # check that it runs $ adb shell ps | grep mrknlogd system 44 1 772 300 c0051854 40012c74 S /system/bin/mrknlogd $ adb logcat | grep MRKN I/MRKN Log Daemon( 37): Flushed log (1, 60 of 65536 bytes). Waiting 60 seconds before the next flush. I/MRKN Log Daemon( 37): Flushed log (2, 34406 of 65536 bytes). Waiting 60 seconds before the next flush. I/MRKN Log Daemon( 37): Flushed log (3, 232 of 65536 bytes). Waiting 60 seconds before the next flush. ^C $ adb logcat -g /dev/log/main: ring buffer is 64Kb (0Kb consumed), max entry is 4096b, max payload is 4076b # good :-)
Since our Java/JNI library will primarily service the Application Framework layer, let’s create a home for our framework components:
$ mkdir device/marakana/alpha/framework
Like with our libraries and executables, we need to "recursively" include makefiles in sub-directories of …/framework/, which is where our Java/JNI library will live:
include $(call all-subdir-makefiles)
Now, let’s create the home for our Java/JNI library and two sub-directories for its Java and JNI parts:
$ mkdir device/marakana/alpha/framework/libmrknlog_jni $ mkdir device/marakana/alpha/framework/libmrknlog_jni/java $ mkdir device/marakana/alpha/framework/libmrknlog_jni/jni
To include these sub-directories in the compilation (java/ and jni/), we again need one of those "recursive" makefiles:
include $(call all-subdir-makefiles)
Now, let’s create the directory structure that reflects our Java library’s package name (com.marakana.android.lib.log):
$ mkdir -p device/marakana/alpha/framework/libmrknlog_jni/java/com/marakana/android/lib/log
Now let’s create our Java "library" class (LibLog.java) and its accompanying exception (LibLogException.java):
package com.marakana.android.lib.log; public class LibLog { private int nativeHandle; public LibLog() { this.init(); } @Override protected void finalize() { this.close(); } private native void init() throws LibLogException; public native void close(); public native void flushLog() throws LibLogException; public native int getTotalLogSize() throws LibLogException; public native int getUsedLogSize() throws LibLogException; public native boolean waitForLogData(int timeoutInMs) throws LibLogException; static { System.loadLibrary("mrknlog_jni"); } }
|
|
As we will see, here the handle represents a pseudo pointer to the HAL module LibLog will allow us to use. |
package com.marakana.android.lib.log; public class LibLogException extends RuntimeException { public LibLogException(String msg) { super(msg); } }
While we are at it, we might as well create a simple Main.java class to test our Java/JNI library:
package com.marakana.android.lib.log; /** @hide */ public class Main { public static void main (String[] args) { try { LibLog libLog = new LibLog(); try { int usedSize = libLog.getUsedLogSize(); int totalSize = libLog.getTotalLogSize(); libLog.flushLog(); System.out.printf("Flushed log. Previously it was consuming %d of %d bytes\n", usedSize, totalSize); } finally { libLog.close(); } } catch (LibLogException e) { System.err.println("Failed to flush the log"); e.printStackTrace(); } } }
For our Java/JNI library (deployed as /system/framework/com.marakana.android.lib.log.jar) to be accessible to the running applications, we need to explicitly expose its logical name (com.marakana.android.lib.log) via a simple XML mapping file (com.marakana.android.lib.log.xml):
<?xml version="1.0" encoding="utf-8"?> <permissions> <library name="com.marakana.android.lib.log" file="/system/framework/com.marakana.android.lib.log.jar"/> </permissions>
|
|
This file will be deployed as /system/etc/permissions/com.marakana.android.lib.log.xml in the target image and applications that wish to use our library will have to reference it in their via AndroidManifest.xml with: <uses-library android:name="com.marakana.android.lib.log" android:required="true"/> |
Now we are ready for the makefile (Android.mk):
LOCAL_PATH := $(call my-dir) # Build the library include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_MODULE := com.marakana.android.lib.log LOCAL_SRC_FILES := $(call all-java-files-under,.) include $(BUILD_JAVA_LIBRARY) # Build the documentation include $(CLEAR_VARS) LOCAL_SRC_FILES := $(call all-subdir-java-files) $(call all-subdir-html-files) LOCAL_MODULE:= com.marakana.android.lib.log_doc LOCAL_DROIDDOC_OPTIONS := com.marakana.android.lib.log LOCAL_MODULE_CLASS := JAVA_LIBRARIES LOCAL_DROIDDOC_USE_STANDARD_DOCLET := true include $(BUILD_DROIDDOC) # Copy com.marakana.android.lib.log.xml to /system/etc/permissions/ include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_MODULE := com.marakana.android.lib.log.xml LOCAL_MODULE_CLASS := ETC LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/permissions LOCAL_SRC_FILES := $(LOCAL_MODULE) include $(BUILD_PREBUILT)
|
|
In this one file, we have three separate targets: to compile the code, to build the documentation (used by the SDK addon), and to copy the com.marakana.android.lib.log.xml file to /system/etc/permissions/ (though this last one seems a too fancy for a simple copy command). Notice that we separate these three targets using the include $(CLEAR_VARS) macro. |
Compile:
$ cd device/marakana/alpha/framework/libmrknlog_jni/java device/marakana/alpha/framework/libmrknlog_jni/java$ mm com.marakana.android.lib.log com.marakana.android.lib.log.xml … target Java: com.marakana.android.lib.log … Copying: out/target/common/obj/JAVA_LIBRARIES/com.marakana.android.lib.log_intermediates/classes.jar … Install: out/target/product/alpha/system/framework/com.marakana.android.lib.log.jar target Prebuilt: com.marakana.android.lib.log.xml (out/target/product/alpha/obj/ETC/com.marakana.android.lib.log.xml_intermediates/com.marakana.android.lib.log.xml) Install: out/target/product/alpha/system/etc/permissions/com.marakana.android.lib.log.xml … device/marakana/alpha/framework/libmrknlog_jni/java$ croot $
If we wanted to implement our JNI code in C, it would probably be easiest to first create the header file(s) using javah tool. But this tool only operates on Java code, not Dex.
Fortunately we could re-use the compiled classes left for us in the out/ directory by the previous step:
$ javah -jni \
-d device/marakana/alpha/framework/libmrknlog_jni/jni/ \
-classpath out/target/common/obj/JAVA_LIBRARIES/com.marakana.android.lib.log_intermediates/classes.jar \
com.marakana.android.lib.log.LibLog
This command would generate a C header file (…_LibLog.h):
… JNIEXPORT void JNICALL Java_com_marakana_android_lib_log_LibLog_flushLog (JNIEnv *, jclass); … JNIEXPORT jint JNICALL Java_com_marakana_android_lib_log_LibLog_getTotalLogSize (JNIEnv *, jclass); … JNIEXPORT jint JNICALL Java_com_marakana_android_lib_log_LibLog_getUsedLogSize (JNIEnv *, jclass); …
Now we could include our header file and provide the implementation.
Now we can provide our implementation (com_marakana_android_lib_log_LibLog.cpp), which initializes our HAL module (mrknlog.default.so), and delegates calls to it while providing some basic error handling:
#include <jni.h> #include <mrknlog.h> #include <hardware/hardware.h> #include "JNIHelp.h" static void throwLibLogException(JNIEnv *env, const char *msg) { jniThrowException(env, "com/marakana/android/lib/log/LibLogException", msg); } static jfieldID nativeHandleFieldId; static void native_init(JNIEnv *env, jobject object) { hw_module_t* module; int err = hw_get_module(MRKNLOG_HARDWARE_MODULE_ID, (hw_module_t const**)&module); if (err) { throwLibLogException(env, "Failed to get module"); } else { struct mrknlog_device_t *dev; err = module->methods->open(module, 0, (struct hw_device_t **) &dev); if (err) { throwLibLogException(env, "Failed to open device"); } else { env->SetIntField(object, nativeHandleFieldId, (jint) dev); } } } static struct mrknlog_device_t * getDevice(JNIEnv *env, jobject object) { return (struct mrknlog_device_t *) env->GetIntField(object, nativeHandleFieldId); } static void native_close(JNIEnv *env, jobject object) { struct mrknlog_device_t *dev = getDevice(env, object); dev->common.close((struct hw_device_t *)dev); } static void flushLog(JNIEnv *env, jobject object) { struct mrknlog_device_t *dev = getDevice(env, object); if (dev->flush_log(dev) != 0) { throwLibLogException(env, "Failed to flush log"); } } static jint getTotalLogSize(JNIEnv *env, jobject object) { struct mrknlog_device_t *dev = getDevice(env, object); int ret = dev->get_total_log_size(dev); if (ret < 0) { throwLibLogException(env, "Failed to get total log size"); } return ret; } static jint getUsedLogSize(JNIEnv *env, jobject object) { struct mrknlog_device_t *dev = getDevice(env, object); int ret = dev->get_used_log_size(dev); if (ret < 0) { throwLibLogException(env, "Failed to get used log size"); } return ret; } static jboolean waitForLogData(JNIEnv *env, jobject object, jint timeoutInMs) { struct mrknlog_device_t *dev = getDevice(env, object); int ret = dev->wait_for_log_data(dev, timeoutInMs); if (ret < 0) { throwLibLogException(env, "Failed while waiting for log data"); } return ret > 0 ? JNI_TRUE : JNI_FALSE; } static JNINativeMethod method_table[] = { { "init", "()V", (void *) native_init }, { "close", "()V", (void *) native_close }, { "flushLog", "()V", (void *) flushLog }, { "getTotalLogSize", "()I", (void *) getTotalLogSize }, { "getUsedLogSize", "()I", (void *) getUsedLogSize }, { "waitForLogData", "(I)Z", (void *) waitForLogData } }; static const char * class_name = "com/marakana/android/lib/log/LibLog"; extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env = NULL; if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) == JNI_OK) { if (jniRegisterNativeMethods(env, class_name, method_table, NELEM(method_table)) == 0) { jclass clazz = env->FindClass(class_name); if (clazz) { nativeHandleFieldId = env->GetFieldID(clazz, "nativeHandle", "I"); if (nativeHandleFieldId) { return JNI_VERSION_1_4; } } } } return JNI_ERR; }
|
|
The Java code will be responsible for maintaining a reference to the HAL module. |
Next, we create a makefile (Android.mk) to compile our JNI code into a shared library (libmrknlog_jni.so):
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := com_marakana_android_lib_log_LibLog.cpp LOCAL_C_INCLUDES += $(JNI_H_INCLUDE) $(LOCAL_PATH)/../../../include/ LOCAL_CFLAGS += -g -O0 LOCAL_SHARED_LIBRARIES := libhardware libnativehelper LOCAL_MODULE := libmrknlog_jni include $(BUILD_SHARED_LIBRARY)
|
|
As with libmrknlog.so, in previous versions of Android (pre-4.0) we would have had to also specify LOCAL_PRELINK_MODULE := false to disable pre-linking of our shared library. |
|
|
Consider taking advantage of JNIHelp.h (implemented by libnativehelper.so) for most simple boilerplate JNI code. |
Now we have all the pieces to compile our JNI library (into /system/lib/libmrknlog_jni.so):
$ cd device/marakana/alpha/framework/libmrknlog_jni/jni device/marakana/alpha/framework/libmrknlog_jni/jni$ mm … Install: out/target/product/alpha/system/lib/libmrknlog_jni.so device/marakana/alpha/framework/libmrknlog_jni/jni$ croot $
As before, since our Java/JNI library’s components are marked as LOCAL_MODULE_TAGS := optional, in order to get them into the final ROM, we need to register them with Alpha’s PRODUCT_PACKAGES:
… PRODUCT_PACKAGES += \ com.marakana.android.lib.log \ com.marakana.android.lib.log.xml \ libmrknlog_jni
Now we can rebuild our entire device:
$ make -j10 … Install system fs image: out/target/product/alpha/system.img
Finally, we are ready to test our Java/JNI library via the Main.main() method:
# (re)start the emulator $ $ANDROID_HOST_OUT/bin/emulator -kernel out/target/product/alpha/kernel & # wait for the emulator to finish # check out our Java library $ adb shell ls -l /system/framework/com.marakana.android.lib.log.jar -rw-r--r-- root root 313 2012-01-05 00:47 com.marakana.android.lib.log.jar # check out our Java library registry file $ adb shell ls -l /system/etc/permissions/com.marakana.android.lib.log.xml -rw-r--r-- root root 180 2012-01-05 00:47 com.marakana.android.lib.log.xml # check out our JNI shared library $ adb shell ls -l /system/lib/libmrknlog_jni.so -rw-r--r-- root root 5452 2012-01-05 00:54 libmrknlog_jni.so # check if our utility is doing what it is supposed to $ adb logcat -g /dev/log/main: ring buffer is 64Kb (33Kb consumed), max entry is 4096b, max payload is 4076b # now run our Java library's Main.main() by directly invoking the Dalvik VM $ adb shell dalvikvm -cp /system/framework/com.marakana.android.lib.log.jar com.marakana.android.lib.log.Main Flushed log. Previously it was consuming 34346 of 65536 bytes # check again $ adb logcat -g /dev/log/main: ring buffer is 64Kb (0Kb consumed), max entry is 4096b, max payload is 4076b $ adb shell dalvikvm -cp /system/framework/com.marakana.android.lib.log.jar com.marakana.android.lib.log.Main Flushed log. Previously it was consuming 318 of 65536 bytes # good :-)
Create a home directory for our Alpha apps:
$ mkdir -p device/marakana/alpha/app/
|
|
This app will not be shared with the SDK add-on. |
As we should know by now, we need a makefile (Android.mk) to include app/'s the sub-directories:
include $(call all-subdir-makefiles)
Now we can create a sub-directory for our app (MrknLogNative), its basic directory structure (res/, and src/), and the source package (com.marakana.android.mrknlognative) directory structure:
$ mkdir -p device/marakana/alpha/app/MrknLogNative/res/values $ mkdir -p device/marakana/alpha/app/MrknLogNative/res/layout $ mkdir -p device/marakana/alpha/app/MrknLogNative/src/com/marakana/android/mrknlognative
We need some basic string resources (used in the UI):
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">Marakana Log Native</string> <string name="log_utilization_message">Using %1$d of %2$d bytes of the log buffer</string> <string name="flush_log_button">Flush Log Buffer</string> </resources>
And we also need a simple layout (log.xml), with a text view, to show the current log utilization, and a button, to allow us to clear the log:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center" android:id="@+id/output" /> <Button android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/button" android:text="@string/flush_log_button"/> </LinearLayout>
Now, we are ready to write our one-and only activity (LogActivity) utilizing com.marakana.android.lib.log Java/JNI library:
package com.marakana.android.mrknlognative; import android.app.Activity; import android.os.Bundle; import android.os.Handler; import android.view.View; import android.widget.Button; import android.widget.TextView; import com.marakana.android.lib.log.LibLog; public class LogActivity extends Activity implements View.OnClickListener, Runnable { private TextView output; private Handler handler; private LibLog libLog; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); super.setContentView(R.layout.log); this.output = (TextView) super.findViewById(R.id.output); Button button = (Button) super.findViewById(R.id.button); button.setOnClickListener(this); this.handler = new Handler(); } @Override public void onResume() { super.onResume(); this.libLog = new LibLog(); this.handler.post(this); } @Override public void onPause() { super.onPause(); this.handler.removeCallbacks(this); this.libLog.close(); } public void onClick(View view) { this.libLog.flushLog(); this.updateOutput(); } public void run() { this.updateOutput(); this.handler.postDelayed(this, 1000); } private void updateOutput() { int usedLogSize = this.libLog.getUsedLogSize(); int totalLogSize = this.libLog.getTotalLogSize(); this.output.setText( super.getString(R.string.log_utilization_message, usedLogSize, totalLogSize)); } }
|
|
Our activity uses a handler in order to request periodic (1 Hz) call-backs to the run() method, via which we simply update our UI with the log utilization data. |
And, like any other app, we need to provide our own AndroidManifest.xml configuration file:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.marakana.android.mrknlognative"> <uses-permission android:name="android.permission.READ_LOGS"/> <application android:label="@string/app_name"> <uses-library android:name="com.marakana.android.lib.log" android:required="true"/> <activity android:name=".LogActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
|
|
We need <uses-permission android:name="android.permission.READ_LOGS"/> so that our app’s user ID gets added to the log group membership, or otherwise we won’t be able to read from /dev/log/main (as discussed earlier). Additionally, we added <uses-library android:name="com.marakana.android.lib.log" android:required="true"/> so that we get run-time access to the com.marakana.android.lib.log library. |
And, like any other component, our app also needs its own makefile (Android.mk):
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := $(call all-java-files-under,src) LOCAL_JAVA_LIBRARIES := com.marakana.android.lib.log LOCAL_PACKAGE_NAME := MrknLogNative include $(BUILD_PACKAGE)
|
|
We are referencing our com.marakana.android.lib.log library here as well, for the sake of the compiler. Also, notice that we are using include $(BUILD_PACKAGE) in order to build our code as an app. |
And, also like any other component that we wish to include in our ROM, we need to register MrknLogNative with the device’s main makefile (full_alpha.xml):
…
PRODUCT_PACKAGES += MrknLogNativeNow we can compile it:
$ make -j10 … Install: out/target/product/alpha/system/app/MrknLogNative.apk … Install system fs image: out/target/product/alpha/system.img
And finally, we can test everything:
Restart the emulator
Launch the Marakana Log Lib Client app
Test that you can flush the log buffer
Rather than have 3rd party applications direct access to "our" driver (via JNI/HAL), we may be better off providing access to the driver’s functionality via a Binder (AIDL-described) service.
|
|
This is of course a moot point, since anyone can write to /dev/log/main. For this to be meaningful, we would need to restrict write access to /dev/log/main via our custom ueventd.marakanaalphaboard.rc file. But, if we did this, we’d break applications' ability to log events. So, for the purpose of this example, we’ll pretend that we’ve "secured" our driver. |
We’ll start off by creating a directory structure for the custom manager library, including the AIDL files used to describe our service:
$ mkdir device/marakana/alpha/framework/mrknlogservice $ mkdir -p device/marakana/alpha/framework/mrknlogservice/com/marakana/android/service/log
Next, we create a AIDL-definition of a client-side listener (ILogListener.aidl):
package com.marakana.android.service.log; /** * Listener for used log size change events. * * {@hide} */ oneway interface ILogListener { void onUsedLogSizeChange(int usedLogSize); }
|
|
This listener will enable the clients to subscribe (via the manager) with the service, to receive notifications of new log events. |
Now, we create an AIDL file to define our service (LibLogService.aidl):
package com.marakana.android.service.log; import com.marakana.android.service.log.ILogListener; /** * System-private API for talking to the LogService. * * {@hide} */ interface ILogService { void flushLog(); int getTotalLogSize(); int getUsedLogSize(); void register(in ILogListener listener); void unregister(in ILogListener listener); }
|
|
Notice that the methods described by ILogService.aidl closely match what our LibLog JNI-bridge provides. |
While the interfaces, proxies, and stubs that will be generated from these AIDL files will be used by both the client and the service, we don’t want to expose this API to the client developers.
Android enables us to use a special {@hide} documentation-level annotation, to specifically remove code from the libraries and documentation that will be exposed to developers via SDK Add-ons, which we will be creating later.
While the hidden methods/classes will not be available during the compile time, they will be available during run time. That means that 3rd-party developers could "cheat" by using reflection to access the hidden APIs. This in turn means that hiding the APIs is generally done just to avoid any possible confusion, and not to enforce any sort of policy (i.e. not for security reasons).
We want to make it easy for app developers to use our service, which means we want to hide to complexity of Binder/threading from them, so we start with an easy-to-implement LogListener:
package com.marakana.android.service.log; /** * Used for receiving notifications from the LogManager when the buffer size has changed. */ public interface LogListener { /** * Called when the buffer size has changed. * Invoked on the main/looper/UI thread. * * @param usedLogSize the new log buffer size. */ public void onUsedLogSizeChange(int usedLogSize); }
|
|
What will make our com.marakana.android.service.log.LogListener easy-to-implement is our promise to call onUsedLogSizeChange(int) from the looper (a.k.a. main/UI) thread as opposed to one of the binder threads. |
Now we can create a LogManager, which will act as the gateway to our (yet-to-be-written) service:
package com.marakana.android.service.log; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; import android.util.Slog; /* This is to avoid generating events for ourselves */ import java.util.HashSet; import java.util.Set; public class LogManager { private static final String TAG = "LogManager"; private static final String REMOTE_SERVICE_NAME = ILogService.class.getName(); private static final boolean DEBUG = false; // change to true to enable debugging private final Set<LogListener> listeners = new HashSet<LogListener>(); private final ILogListener listener = new ILogListener.Stub() { //public void onUsedLogSizeChange(final int usedLogSize) { //
if (DEBUG) Slog.d(TAG, "onUsedLogSizeChange: " + usedLogSize); Message message = LogManager.this.handler.obtainMessage(); message.arg1 = usedLogSize; LogManager.this.handler.sendMessage(message); } }; private final Handler handler = new Handler() { //
@Override public void handleMessage(Message message) { //
int usedLogSize = message.arg1; if (DEBUG) Slog.d(TAG, "Notifying local listeners: " + usedLogSize); synchronized(LogManager.this.listeners) { //
for (LogListener logListener : LogManager.this.listeners) { if (DEBUG) Slog.d(TAG, "Notifying local listener [" + logListener + "] of more used data: " + usedLogSize); logListener.onUsedLogSizeChange(usedLogSize); //
} } } }; private final ILogService service; public static LogManager getInstance() { return new LogManager(); } private LogManager() { Log.d(TAG, "Connecting to ILogService by name [" + REMOTE_SERVICE_NAME + "]"); this.service = ILogService.Stub.asInterface( ServiceManager.getService(REMOTE_SERVICE_NAME)); //
if (this.service == null) { throw new IllegalStateException("Failed to find ILogService by name [" + REMOTE_SERVICE_NAME + "]"); } } public void flushLog() { try { if (DEBUG) Slog.d(TAG, "Flushing log."); this.service.flushLog(); //
} catch (RemoteException e) { throw new RuntimeException("Failed to flush log", e); } } public int getTotalLogSize() { try { if (DEBUG) Slog.d(TAG, "Getting total log size."); return this.service.getTotalLogSize(); //
} catch (RemoteException e) { throw new RuntimeException("Failed to get total log size", e); } } public int getUsedLogSize() { try { if (DEBUG) Slog.d(TAG, "Getting used log size."); return this.service.getUsedLogSize(); //
} catch (Exception e) { throw new RuntimeException("Failed to get used log size", e); } } public void register(LogListener listener) { if (listener != null) { synchronized(this.listeners) { //
if (this.listeners.contains(listener)) { Log.w(TAG, "Already registered: " + listener); } else { try { boolean registerRemote = this.listeners.isEmpty(); if (DEBUG) Log.d(TAG, "Registering local listener."); this.listeners.add(listener); if (registerRemote) { if (DEBUG) Log.d(TAG, "Registering remote listener."); this.service.register(this.listener); //
} } catch (RemoteException e) { throw new RuntimeException("Failed to register " + listener, e); } } } } } public void unregister(LogListener listener) { if (listener != null) { synchronized(this.listeners) { //
if (!this.listeners.contains(listener)) { Log.w(TAG, "Not registered: " + listener); } else { if (DEBUG) Log.d(TAG, "Unregistering local listener."); this.listeners.remove(listener); } if (this.listeners.isEmpty()) { try { if (DEBUG) Log.d(TAG, "Unregistering remote listener."); this.service.unregister(this.listener); //
} catch (RemoteException e) { throw new RuntimeException("Failed to unregister " + listener, e); } } } } } }
| This is the implementation of our ILogListener (binder service), which will handle the notifications from the remote ILogService. | |
| This method will be invoked on the binder thread, so we cannot just call the client’s LogListener. Instead, we send the notification to the looper thread via the handler. | |
| This is our handle onto the looper thread’s message queue. The association with the looper is implicit, because this handler object is instantiated by the looper thread. | |
| This method will be invoked by the looper thread in response to the message sent to us by the listener. | |
| The handler is guaranteed to run "single-threaded", but the client may register/unregister listeners concurrently, so we should to protect the shared set of listeners. | |
| Finally we can notify the client’s LogListener on the looper thread. | |
| Get access to the remote log service (directly, using android.os.ServiceManager) and build a proxy around it. | |
| Use the remote service, and handle RemoteException-s. |
For our clients to access LogManager, we need to expose it as a Java library - so we create an XML descriptor for it:
<?xml version="1.0" encoding="utf-8"?> <permissions> <library name="com.marakana.android.service.log" file="/system/framework/com.marakana.android.service.log.jar"/> </permissions>
Now we are ready ready to create a makefile (Android.mk) with rules to compile our library, build its documentation, and copy its com.marakana.android.service.log.xml descriptor to /system/etc/permissions/:
LOCAL_PATH := $(call my-dir) # Build the library include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_MODULE := com.marakana.android.service.log LOCAL_SRC_FILES := $(call all-java-files-under,.) LOCAL_SRC_FILES += com/marakana/android/service/log/ILogListener.aidl LOCAL_SRC_FILES += com/marakana/android/service/log/ILogService.aidl include $(BUILD_JAVA_LIBRARY) # Build the documentation include $(CLEAR_VARS) LOCAL_SRC_FILES := $(call all-subdir-java-files) $(call all-subdir-html-files) LOCAL_MODULE:= com.marakana.android.service.log_doc LOCAL_DROIDDOC_OPTIONS := com.marakana.android.service.log LOCAL_MODULE_CLASS := JAVA_LIBRARIES LOCAL_DROIDDOC_USE_STANDARD_DOCLET := true include $(BUILD_DROIDDOC) # Copy com.marakana.android.service.log.xml to /system/etc/permissions/ include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_MODULE := com.marakana.android.service.log.xml LOCAL_MODULE_CLASS := ETC LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/permissions LOCAL_SRC_FILES := $(LOCAL_MODULE) include $(BUILD_PREBUILT)
|
|
Notice how we add our .aidl files as LOCAL_SRC_FILES when we BUILD_JAVA_LIBRARY. |
To test that we did everything correctly, we can compile our library:
$ cd device/marakana/alpha/framework/mrknlogservice device/marakana/alpha/framework/mrknlogservice$ mm … Aidl: com.marakana.android.service.log <= device/marakana/alpha/framework/mrknlogservice/com/marakana/android/service/log/ILogListener.aidl Aidl: com.marakana.android.service.log <= device/marakana/alpha/framework/mrknlogservice/com/marakana/android/service/log/ILogService.aidl target Java: com.marakana.android.service.log … target Jar: com.marakana.android.service.log … target Prebuilt: com.marakana.android.service.log.xml … Install: out/target/product/alpha/system/etc/permissions/com.marakana.android.service.log.xml … device/marakana/alpha/framework/mrknlogservice$ croot $
For our com.marakana.android.service.log library to be included in the final ROM, we need to add it to the packages.mk makefile:
… PRODUCT_PACKAGES += \ com.marakana.android.service.log \ com.marakana.android.service.log.xml
Now we can create our MrknLogService, which will implement ILogService, but since we’ll define our service as an application, we need first to create a directory for alpha apps:
$ mkdir -p device/marakana/alpha/app/
|
|
This directory may already exist if we created MrknLogNative app before (this was optional). |
And, as we know by now, we need a makefile to include app/'s sub-directories into the build:
include $(call all-subdir-makefiles)
|
|
This file may already exist if we created MrknLogNative app before (this was optional). |
Next, we create a directory for our service (MrknLogService), its basic directory structure (res/, and src/), and the source package (com.marakana.android.mrknlognative) directory structure:
$ mkdir -p device/marakana/alpha/app/MrknLogService/res/values $ mkdir -p device/marakana/alpha/app/MrknLogService/src/com/marakana/android/logservice
We need some basic string resources, which we’ll use for a custom permission we’ll be creating next:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="flush_log_permission_label">Flush Log</string> <string name="flush_log_permission_description">Applications with this permission will be able to clear the logs, potentially covering their own tracks of malicious behavior.</string> </resources>
Next, let’s define our app’s AndroidManifest.xml file:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.marakana.android.logservice" android:sharedUserId="android.uid.system"> <!----> <permission android:name="com.marakana.android.logservice.FLUSH_LOG" android:protectionLevel="dangerous" android:permissionGroup="android.permission-group.SYSTEM_TOOLS" android:label="@string/flush_log_permission_label" android:description="@string/flush_log_permission_description"/> <!--
--> <uses-permission android:name="android.permission.READ_LOGS"/> <!--
--> <application android:name=".LogServiceApp" android:persistent="true"> <!--
--> <uses-library android:name="com.marakana.android.service.log" /> <!--
--> <uses-library android:name="com.marakana.android.lib.log" /> <!--
--> </application> </manifest>
| Specifying android:sharedUserId="android.uid.system" will enable our service application to run as the system user. We’ll also need to sign our application with the platform key, for this to work. As the result, we’ll be able to register our service with the servicemanager since it explicitly permits the system user: frameworks/base/cmds/servicemanager/service_manager.c:svc_can_register(…). | |
| We create a custom permission, which our service will enforce. | |
| We need to be able to read the logs in order to get current/total log size, etc. | |
| By marking our app as android:persistent="true", we are asking the ActivityManagerService to automatically start our app on boot, keep it running (oomAdj=-12) and restart it if it crashes. The application .LogServiceApp will serve as the bootstraper for the actual service. | |
| We need to use com.marakana.android.service.log, in order to get access to the ILogService and ILogListener interfaces, stubs, and proxies. | |
| We need to use com.marakana.android.lib.log, in order to get access to the JNI wrapper, and transitively, to the HAL/driver. |
Next, we implement (ILogService) by creating ILogServiceImpl:
package com.marakana.android.logservice; import android.content.Context; import android.content.pm.PackageManager; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import com.marakana.android.service.log.ILogListener; import com.marakana.android.service.log.ILogService; import com.marakana.android.lib.log.LibLog; import com.marakana.android.lib.log.LibLogException; class ILogServiceImpl extends ILogService.Stub { private static final String TAG = "ILogServiceImpl"; private static final int INCREMENTAL_TIMEOUT = 2 * 1000; private static final boolean DEBUG = false; private final Map<IBinder, ListenerTracker> listeners = new HashMap<IBinder, ListenerTracker>(); //private final AtomicInteger flushCounter = new AtomicInteger(); //
private final Context context; private final LibLog libLog; //
private LogServiceThread logServiceThread; //
ILogServiceImpl(Context context) { this.context = context; this.libLog = new LibLog(); //
} protected void finalize() throws Throwable { this.libLog.close(); //
super.finalize(); } public void flushLog() { this.context.enforceCallingOrSelfPermission( Manifest.permission.FLUSH_LOG, "Flush somewhere else"); //
if (DEBUG) Slog.d(TAG, "Flushing log."); this.libLog.flushLog(); //
this.flushCounter.incrementAndGet(); //
} public int getUsedLogSize() { if (DEBUG) Slog.d(TAG, "Getting used log size."); return this.libLog.getUsedLogSize(); //
} public int getTotalLogSize() { if (DEBUG) Slog.d(TAG, "Getting total log size."); return this.libLog.getTotalLogSize(); //
} public void register(ILogListener listener) throws RemoteException { if (listener != null) { IBinder binder = listener.asBinder(); synchronized(this.listeners) { //
if (this.listeners.containsKey(binder)) { Slog.w(TAG, "Ignoring duplicate listener: " + binder); } else { ListenerTracker listenerTracker = new ListenerTracker(listener); binder.linkToDeath(listenerTracker, 0); this.listeners.put(binder, listenerTracker); //
if (DEBUG) Slog.d(TAG, "Registered listener: " + binder); if (this.logServiceThread == null) { if (DEBUG) Slog.d(TAG, "Starting thread"); this.logServiceThread = new LogServiceThread(); //
this.logServiceThread.start(); //
} } } } } public void unregister(ILogListener listener) { if (listener != null) { IBinder binder = listener.asBinder(); synchronized(this.listeners) { //
ListenerTracker listenerTracker = this.listeners.remove(binder); //
if (listenerTracker == null) { Slog.w(TAG, "Ignoring unregistered listener: " + binder); } else { if (DEBUG) Slog.d(TAG, "Unregistered listener: " + binder); binder.unlinkToDeath(listenerTracker, 0); if (this.logServiceThread != null && this.listeners.isEmpty()) { if (DEBUG) Slog.d(TAG, "Stopping thread"); this.logServiceThread.interrupt(); //
this.logServiceThread = null; //
} } } } } protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (this.context.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) != PackageManager.PERMISSION_GRANTED) { pw.print("Permission Denial: can't dump ILogService from from pid="); pw.print(Binder.getCallingPid()); pw.print(", uid="); pw.println(Binder.getCallingUid()); return; } else if (args.length > 0 && args[0] != null) { if (args[0].equals("flush-count")) { pw.println(this.flushCounter.get()); } else if (args[0].equals("used-size")) { pw.println(this.getUsedLogSize()); } else if (args[0].equals("total-size")) { pw.println(this.getTotalLogSize()); } else if (args[0].equals("listeners")) { pw.println(this.listeners.size()); } else { pw.println("Usage: ILogService [flush-count|used-size|total-size|listeners]"); } } else { pw.println("ILogServiceState:"); pw.print("Flush count: "); pw.println(this.flushCounter.get()); pw.print("Used log size: "); pw.println(this.getUsedLogSize()); pw.print("Total log size: "); pw.println(this.getTotalLogSize()); pw.print("Listeners: "); pw.println(this.listeners.size()); } } private final class ListenerTracker implements IBinder.DeathRecipient { private final ILogListener listener; public ListenerTracker(ILogListener listener) { this.listener = listener; } public ILogListener getListener() { return this.listener; } public void binderDied() { ILogServiceImpl.this.unregister(this.listener); } } private final class LogServiceThread extends Thread { //
public void run() { while(!Thread.interrupted()) { //
try { if (DEBUG) Slog.d(TAG, "Waiting for log data"); int usedSize = ILogServiceImpl.this.getUsedLogSize(); if (ILogServiceImpl.this.libLog.waitForLogData(INCREMENTAL_TIMEOUT) || (usedSize != 0 && ILogServiceImpl.this.getUsedLogSize() == 0)) { usedSize = ILogServiceImpl.this.getUsedLogSize(); if (DEBUG) Slog.d(TAG, "Log data changed. Used data is now at " + usedSize); synchronized(ILogServiceImpl.this.listeners) { for (Map.Entry<IBinder, ListenerTracker> entry : ILogServiceImpl.this.listeners.entrySet()) { ILogListener listener = entry.getValue().getListener(); try { if (DEBUG) Slog.d(TAG, "Notifying listener: " + entry.getKey()); listener.onUsedLogSizeChange(usedSize); } catch (RemoteException e) { Slog.e(TAG, "Failed to update listener: " + entry.getKey(), e); ILogServiceImpl.this.unregister(listener); } } } } } catch (LibLogException e) { Slog.e(TAG, "Oops", e); try { Thread.sleep(INCREMENTAL_TIMEOUT); } catch (InterruptedException e2) { break; } } } } } }
|
|
For the most part, our service simply wraps our JNI library, except that it requires that the caller be granted our custom Manifest.permission.FLUSH_LOG permission before calling the LibLog.flushLog() method. |
For our ILogServiceImpl to be accessible to our LogManager, we need to instantiate it and register it with the ServiceManager - we do this in our custom LogServiceApp application class loaded from the manifest file:
package com.marakana.android.logservice; import android.app.Application; import android.os.ServiceManager; import android.util.Log; import com.marakana.android.service.log.ILogService; public class LogServiceApp extends Application { private static final String TAG = "LogServiceApp"; private static final String REMOTE_SERVICE_NAME = ILogService.class.getName(); private ILogServiceImpl serviceImpl; public void onCreate() { super.onCreate(); this.serviceImpl = new ILogServiceImpl(this); ServiceManager.addService(REMOTE_SERVICE_NAME, this.serviceImpl); Log.d(TAG, "Registered [" + serviceImpl.getClass().getName() + "] as [" + REMOTE_SERVICE_NAME + "]"); } public void onTerminate() { super.onTerminate(); Log.d(TAG, "Terminated"); } }
|
|
Normally, regular applications cannot get access to ServiceManager class (because it is hidden) nor to the servicemanager daemon (because it enforces UID-based restrictions). To work around these limitations, we’ll compile our app with the framework classes (where ServiceManager is not hidden), and run it with the system user (which does have access to servicemanager daemon). |
We are now ready for our makefile (Android.mk):
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := $(call all-java-files-under,src) LOCAL_REQUIRED_MODULES := \ com.marakana.android.service.log \ com.marakana.android.lib.log LOCAL_JAVA_LIBRARIES := \ com.marakana.android.service.log \ com.marakana.android.lib.log \ core \ framework LOCAL_PACKAGE_NAME := MrknLogService LOCAL_SDK_VERSION := current LOCAL_PROGUARD_ENABLED := disabled LOCAL_CERTIFICATE := platform include $(BUILD_PACKAGE)
|
|
The setting LOCAL_JAVA_LIBRARIES := … framework allows us to reference ServiceManager and LOCAL_CERTIFICATE := platform makes it possible for us to run with the system user and thereby access the servicemanager daemon. |
Let’s compile our code:
$ cd device/marakana/alpha/app/MrknLogService device/marakana/alpha/app/MrknLogService$ mm … Install: out/target/product/alpha/system/app/MrknLogService.apk device/marakana/alpha/app/MrknLogService$ croot $
We want our MrknLogService to be included in the final ROM, so we add it to packages.mk:
…
PRODUCT_PACKAGES += MrknLogServiceCompile the entire device:
$ make -j10 … Install system fs image: out/target/product/alpha/system.img
It’s best to test our new manager→service→library→driver proxy via a real application, so that’s what we’ll be creating next.
Create a new directory for our new app (MrknLogClient), its basic directory structure (res/, and src/), and the source package (com.marakana.android.mrknlogclient) directory structure:
$ mkdir -p device/marakana/alpha/app/MrknLogClient/res/values $ mkdir -p device/marakana/alpha/app/MrknLogClient/res/layout $ mkdir -p device/marakana/alpha/app/MrknLogClient/src/com/marakana/android/mrknlogclient
Just like before, we need some basic string resources (used in the UI):
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">Marakana Log Client</string> <string name="log_utilization_message">Using %1$d of %2$d bytes of the log buffer</string> <string name="flush_log_button">Flush Log Buffer</string> </resources>
And we also need a simple layout (log.xml), which is exactly the same as the one we created for MrknLogNative before:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center" android:id="@+id/output" /> <Button android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/button" android:text="@string/flush_log_button"/> </LinearLayout>
Our activity (LogActivity) will be similar to the one we wrote before, except that in this case will be using com.marakana.android.service.log Java (Binder) library and its LogManager APIs:
package com.marakana.android.mrknlogclient; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.TextView; import com.marakana.android.service.log.LogListener; import com.marakana.android.service.log.LogManager; public class LogActivity extends Activity implements OnClickListener, LogListener { private TextView output; private final LogManager logManager = LogManager.getInstance(); private final int totalLogSize = this.logManager.getTotalLogSize(); public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); super.setContentView(R.layout.log); this.output = (TextView)super.findViewById(R.id.output); Button button = (Button)super.findViewById(R.id.button); button.setOnClickListener(this); } private void updateOutput(int usedLogSize) { this.output.setText(super.getString(R.string.log_utilization_message, usedLogSize, this.totalLogSize)); } @Override public void onResume() { super.onResume(); this.logManager.register(this); this.updateOutput(this.logManager.getUsedLogSize()); } @Override public void onPause() { super.onPause(); this.logManager.unregister(this); } @Override public void onClick(View view) { this.logManager.flushLog(); this.updateOutput(0); } @Override public void onUsedLogSizeChange(int usedLogSize) { this.updateOutput(usedLogSize); } }
Like before, we need a AndroidManifest.xml file, but this time around we are using the ….FLUSH_LOG permission, and com.marakana.android.service.log library:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.marakana.android.mrknlogclient"> <uses-permission android:name="com.marakana.android.logservice.FLUSH_LOG" /> <application android:label="@string/app_name"> <uses-library android:name="com.marakana.android.service.log" android:required="true" /> <activity android:name=".LogActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
And, also like before, our app needs its own makefile (Android.mk):
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := $(call all-java-files-under,src) LOCAL_JAVA_LIBRARIES := com.marakana.android.service.log LOCAL_PACKAGE_NAME := MrknLogClient include $(BUILD_PACKAGE)
|
|
We are referencing our com.marakana.android.service.log library here as well, for the sake of the compiler. |
And, as before, we need to register MrknLogClient with the device’s main makefile (full_alpha.xml) in order to get it included in the ROM:
…
PRODUCT_PACKAGES += MrknLogClientNow we can compile our entire device:
$ make -j10 … Install: out/target/product/alpha/system/app/MrknLogClient.apk … Install system fs image: out/target/product/alpha/system.img
And finally, we can test everything:
Restart the emulator
Launch the Marakana Log Service Client app
Test that you can flush the log buffer
Create a directory for our addon:
$ mkdir device/marakana/alpha_sdk_addon
Though this is purely optional, let’s create a custom emulator skin for our add-on, by copying an existing one:
$ mkdir device/marakana/alpha_sdk_addon/skins $ cp -r sdk/emulator/skins/HVGA device/marakana/alpha_sdk_addon/skins/MrknHvgaMdpi
We could now customize the portrait background of our skin (device/marakana/alpha_sdk_addon/skins/MrknHvgaMdpi/background_port.png) - say by adding our custom logo to it
|
|
For example, we could use this one: https://raw.github.com/marakana/alpha_sdk_addon/master/skins/MrknHvgaMdpi/background_land.png |
We could also customize the landscape background of our skin (device/marakana/alpha_sdk_addon/skins/MrknHvgaMdpi/background_land.png) - also by adding our custom logo to it
|
|
For example, we could use this one: https://raw.github.com/marakana/alpha_sdk_addon/master/skins/MrknHvgaMdpi/background_port.png |
We could also customize other images, layout controls (layout), and hardware-specific values (hardware.ini)
To simulate that our devices supports NFC, we would:
Create a directory (nxp):
$ mkdir device/marakana/alpha_sdk_addon/nxp
Add a configuration file (com.nxp.mifare.xml) declaring NFC-feature support:
<?xml version="1.0" encoding="utf-8"?> <!-- Devices that support MIFARE need to declare NXP's feature constant --> <permissions> <feature name="com.nxp.mifare" /> </permissions>
|
|
NFC-support is not really required for our particular product (Alpha), but this is an example of how some hardware-specific features are defined. |
While on the subject of hardware, let’s define skin-independent set of hardware properties (hardware.ini) for our emulated device:
# Custom hardware options for the add-on. # Properties defined here impact all AVD targeting this add-on. # Each skin can also override those values with its own hardware.ini file. vm.heapSize = 24
Next, we define the properties of our SDK addon (manifest.ini) - name, description, API version, revision, libraries, and skin - as these properties are required by development tools (such as SDK Manager and Eclipse) using our addon:
name=Alpha Add-On vendor=Marakana description=Marakana Alpha Add-on api=15 revision=1 libraries=com.marakana.android.lib.log;com.marakana.android.service.log com.marakana.android.lib.log=com.marakana.android.lib.log.jar;Marakana Log Library com.marakana.android.service.log=com.marakana.android.service.log.jar;Marakana Log Service skin=MrknHvgaMdpi
Next, we define a list of classes to be included (+<package-name>.(*|<class-name>)) or excluded (-<package-name>.(\*|<class-name>)) from the generated SDK addon’s libraries:
+com.marakana.android.lib.log.* -com.marakana.android.lib.log.Main +com.marakana.android.service.log.*
|
|
These classes are known as SDK Addon Stub Definitions, and this file is processed by development/tools/mkstubs during the build time. The Java tool mkstubs loads this file via PRODUCT_SDK_ADDON_STUB_DEFS makefile property. |
Now we are ready for our addon’s main makefile (alpha_sdk_addon.mk), which will be loaded later by AndroidProducts.mk:
# Include the common stuff include $(LOCAL_PATH)/../alpha/common.mk # List of modules to include in the the add-on system image #PRODUCT_PACKAGES += # The name of this add-on (for the SDK) PRODUCT_SDK_ADDON_NAME := marakana_alpha_addon # Copy the following files for this add-on's SDK PRODUCT_SDK_ADDON_COPY_FILES := \ $(LOCAL_PATH)/manifest.ini:manifest.ini \ $(LOCAL_PATH)/hardware.ini:hardware.ini \ $(call find-copy-subdir-files,*,$(LOCAL_PATH)/skins/MrknHvgaMdpi,skins/MrknHvgaMdpi) # Copy the jar files for the libraries (APIs) exposed in this add-on's SDK PRODUCT_SDK_ADDON_COPY_MODULES := \ com.marakana.android.lib.log:libs/com.marakana.android.lib.log.jar \ com.marakana.android.service.log:libs/com.marakana.android.service.log.jar PRODUCT_SDK_ADDON_STUB_DEFS := $(LOCAL_PATH)/alpha_sdk_addon_stub_defs.txt # Define the name of the documentation to generate for this add-on's SDK PRODUCT_SDK_ADDON_DOC_MODULES := \ com.marakana.android.service.log_doc # Since the add-on is an emulator, we also need to explicitly copy the kernel to images PRODUCT_SDK_ADDON_COPY_FILES += $(LOCAL_KERNEL):images/armeabi-v7a/kernel-qemu # This add-on extends the default sdk product. $(call inherit-product, $(SRC_TARGET_DIR)/product/sdk.mk) # The name of this add-on (for the build system) # Use 'make PRODUCT-<PRODUCT_NAME>-sdk_addon' to build the an add-on, # so in this case, we would run 'make PRODUCT-marakana_alpha_addon-sdk_addon' PRODUCT_NAME := marakana_alpha_addon PRODUCT_DEVICE := alpha PRODUCT_MODEL := Marakana Alpha SDK Addon Image for Emulator
|
|
As we can see from the comments, this file extends from build/target/product/sdk.mk, includes everything from alpha-commmon, adds support for library documentation, defines the addon’s name (PRODUCT_SDK_ADDON_NAME), specifies the files/modules to be copied (manifest.ini, hardware.ini, skin, libraries, and kernel) into the addon (PRODUCT_SDK_ADDON_COPY_FILES), loads stub defs, and defines the addon’s product info (like PRODUCT_NAME). |
Just like with our alpha product, we need to create AndroidProducts.mk file, which the build system looks for to get a list of the actual make files (in this just alpha_sdk_addon.mk) to process for this "product" (i.e. SDK addon):
PRODUCT_MAKEFILES := $(LOCAL_DIR)/alpha_sdk_addon.mk
Just to be on the safe side, let’s make sure that make files from all sub-directories also get included:
include $(call all-subdir-makefiles)
|
|
This file is unnecessary in this particular case, but it does not hurt to have it in case we add more addon-specific components down the road, which will most likely be stored in their own sub-directories. |
Now we are ready to compile our addon:
$ make -j10 PRODUCT-marakana_alpha_addon-sdk_addon … Copy: out/host/linux-x86/obj/SDK_ADDON/marakana_alpha_addon_intermediates/marakana_alpha_addon-eng.sasa-linux-x86/images/armeabi-v7a/system.img Packaging SDK Addon: out/host/linux-x86/sdk_addon/marakana_alpha_addon-eng.sasa-linux-x86.zip
|
|
Notice that the directory/file-name encodes the name of the user who built this addon (in this case "sasa"). Adjust the following steps as necessary. |
Next, to test that it works, we first install our SDK to our Android SDK’s add-ons/ directory:
$ unzip out/host/linux-x86/sdk_addon/marakana_alpha_addon-eng.sasa-linux-x86.zip -d /home/sasa/android/sdk/add-ons/ Archive: out/host/linux-x86/sdk_addon/marakana_alpha_addon-eng.sasa-linux-x86.zip extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/select.png extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/arrow_left.png inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/background_land.png extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/arrow_right.png inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/layout inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/background_port.png extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/hardware.ini inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/spacebar.png inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/controls.png inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/key.png inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/keyboard.png extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/button.png extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/arrow_down.png extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/arrow_up.png extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/package-list inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/help-doc.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/com/marakana/android/service/log/package-summary.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/com/marakana/android/service/log/package-frame.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/com/marakana/android/service/log/package-tree.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/com/marakana/android/service/log/LogManager.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/allclasses-noframe.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/constant-values.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/stylesheet.css inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/deprecated-list.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/index.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/allclasses-frame.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/index-all.html extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/resources/inherit.gif inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/overview-tree.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/images/armeabi-v7a/build.prop inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/images/armeabi-v7a/ramdisk.img inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/images/armeabi-v7a/kernel-qemu inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/images/armeabi-v7a/NOTICE.txt inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/images/armeabi-v7a/userdata.img inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/images/armeabi-v7a/system.img inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/hardware.ini inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/manifest.ini inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/libs/com.marakana.android.lib.log.jar inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/libs/com.marakana.android.service.log.jar
|
|
Adjust the path to Android SDK directory as necessary. |
Let’s test that it shows up in our Android SDK and ADV Manager:
Run
$ /path/to/android/sdk/tools/android $
|
|
Adjust the path to Android SDK directory as necessary. |
And we should now see "Alpha Add-On by Marakana, Android API 15, revision 1, Installed" show up under Packages:
|
|
For this to work, we need to have our addon’s api setting (in manifest.ini file) match an available SDK Platform Android of the same API version. |
Now we can create a new emulator image (AVD) based on our add-on (from the Android SDK and AVD Manager):
To see that it works, we can now launch our newly created "marakana-alpha" AVD:
Our marakana-alpha AVD will not have any of the Mrkn*ClientApp apps we previously created for the "Marakana Alpha" device , but since our APIs are available to us via the libraries, we can re-create these applications in Eclipse and deploy them to our AVD - this is left as an exercise for the reader :-)
First, we need to create a repository.xml file to describe our add-on and publish it to our server:
<?xml version="1.0" encoding="UTF-8"?> <sdk:sdk-addon xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sdk="http://schemas.android.com/sdk/android/addon/1"> <sdk:add-on> <sdk:name>Alpha Add-On</sdk:name> <sdk:api-level>15</sdk:api-level> <sdk:vendor>Marakana</sdk:vendor> <sdk:revision>1</sdk:revision> <sdk:description>Android + Marakana Alpha Add-on, API 15, revision 1 </sdk:description> <sdk:desc-url>http://marakana.com/external/android/sdk-addon/</sdk:desc-url> <sdk:uses-license ref="marakana-android-addon-license" /> <sdk:archives> <sdk:archive os="any"> <sdk:size>96382546</sdk:size> <sdk:checksum type="sha1">5cec8cc3f3064441cb96a50e2b8aa528681ffc78</sdk:checksum> <sdk:url>marakana_alpha_sdk_addon_api-15_r1.zip</sdk:url> </sdk:archive> </sdk:archives> <sdk:libs> </sdk:libs> </sdk:add-on> <sdk:license type="text" id="marakana-android-addon-license"> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this SDK addon except in compliance with the License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. </sdk:license> </sdk:sdk-addon>
|
|
We have to make sure that we adjust the info as necessary to reflect our particular site/addon. |
Next, we need to upload our addon to our site, relative to <sdk:sdk-addon> → <sdk:add-on> → <sdk:desc-url> URL (e.g. https://marakana.com/external/android/sdk-addon/marakana_alpha_sdk_addon_api-15_r1.zip)
We are now ready to test it out in our Android SDK Manager
Go to Tools → Manage Add-on Sites… in the menu bar
Click on New…
Enter the base URL in the URL field (e.g. https://marakana.com/external/android/sdk-addon/) and click on OK
Click on Close to dismiss the Add-on Sites window
Under Packages we should now see Alpha Add-On by Marakana, with a status Not installed
Click on the checkbox next to our package
Click on Install 1 package… button
Accept the license terms and click on Install
Test that it works by creating an AVD based on our add-on and/or using it in Eclipse
What is a home directory for a new custom board within AOSP source-tree?
What is the purpose of vendorsetup.sh?
What does $(call inherit-product, $(SRC_TARGET_DIR)/product/generic.mk) do?
What is the significance of PRODUCT_PACKAGES?
What is the purpose of BoardConfig.mk?
What is the purpose of development/tools/make_key and why do we want to use it?
What is the significance of /proc/cpuinfo?
When/why do we use include $(call all-subdir-makefiles)?
What is the purpose of LOCAL_MODULE?
What is the purpose of LOCAL_MODULE_TAGS?
How do we compile individual modules efficiently?
Name at least three include $(BUILD_XXX) targets.
What do we need to do in order to launch a custom daemon on startup?
How do we expose custom Java system libraries to applications?
How do applications consume custom Java system libraries?
When do we use include $(BUILD_PREBUILT)?
When/where do we use $(JNI_H_INCLUDE)?
What is the purpose of {@hide}?
When do we use include $(BUILD_DROIDDOC)?
What do we need to do in order to register a custom service to servicemanager?
When/why would we use LOCAL_JAVA_LIBRARIES += framework?
When/why would we use LOCAL_CERTIFICATE := platform?
What does include $(BUILD_PACKAGE) build?
What is SDK add-on and when/why would we want to build/use one?
How do we distribute custom SDK add-ons?
Android supports a variety of USB peripherals and Android USB accessories (hardware that implements the Android accessory protocol) through two modes: USB host and USB accessory.
In USB host mode, the Android-powered device acts as the host.
In USB accessory mode, the external USB hardware act as the USB hosts.
USB accessory and host modes are directly supported in Android 3.1 (API level 12) or newer platforms.
|
|
Support for USB host and accessory modes are ultimately dependent on the device’s hardware, regardless of platform level. You can filter for devices that support USB host and accessory through a <uses-feature> element, as discussed later. |
The android.hardware.usb package contains the following classes supporting USB host mode:
Allows you to enumerate and communicate with connected USB devices.
Represents a connected USB device and contains methods to access its identifying information, interfaces, and endpoints.
Represents an interface of a USB device, which defines a set of functionality for the device. A device can have one or more interfaces on which to communicate on.
Represents an interface endpoint, which is a communication channel for this interface. An interface can have one or more endpoints, and usually has input and output endpoints for two-way communication with the device.
Represents a connection to the device, which transfers data on endpoints. This class allows you to send data back and forth sychronously or asynchronously.
Represents an asynchronous request to communicate with a device through a UsbDeviceConnection.
Defines USB constants that correspond to definitions in linux/usb/ch9.h of the Linux kernel.
In general, you:
Obtain a UsbManager to retrieve the desired UsbDevice
Find the appropriate UsbInterface and the UsbEndpoint of that interface to communicate on
Open a UsbDeviceConnection to communicate with the USB device
In most situations, you need to use all of these classes (UsbRequest is only required if you are doing asynchronous communication) when communicating with a USB device.
Not all Android-powered devices are guaranteed to support the USB host APIs.
To indicate that your application requires USB host support, add the following elements to your application’s manifest:
<uses-feature android:name="android.hardware.usb.host" />
<uses-sdk android:minSdkVersion="12" />
When users connect USB devices to an Android-powered device, the Android system can determine whether your application is interested in the connected device. If so, you can set up communication with the device if desired.
To do this, your application has to:
Discover connected USB devices either by:
Ask the user for permission to connect to the USB device, if not already obtained.
Communicate with the USB device by reading and writing data on the appropriate interface endpoints.
To have your application discover a particular USB device, you can specify an intent filter to filter for the android.hardware.usb.action.USB_DEVICE_ATTACHED intent.
In your activity, you can obtain the UsbDevice that represents the attached device from the intent like this:
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
The following example shows how to declare the intent filter in your application’s manifest:
<activity ...> ... <intent-filter> <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:resource="@xml/device_filter" /> </activity>
The <meta-data> element points to an external XML resource file that declares identifying information about the device that you want to detect.
In the XML resource file, declare <usb-device> elements for the USB devices that you want to filter.
<?xml version="1.0" encoding="utf-8"?> <resources> <usb-device vendor-id="1234" product-id="5678" /> </resources>
Save the resource file in your application’s res/xml/ directory.
If your application is interested in inspecting all of the USB devices currently connected while your application is running, it can enumerate devices on the bus.
UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE); // ... HashMap<String, UsbDevice> deviceList = manager.getDeviceList(); UsbDevice device = deviceList.get("deviceName");
If desired, you can also just obtain an iterator from the hash map and process each device one by one:
UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE); // ... HashMap<String, UsbDevice> deviceList = manager.getDeviceList(); Iterator<UsbDevice> deviceIterator = deviceList.values().iterator(); while (deviceIterator.hasNext()) { UsbDevice device = deviceIterator.next() // ... }
Before communicating with the USB device, your application must have permission from your users.
|
|
If your application uses an intent filter to discover USB devices as they’re connected, it automatically receives permission if the user allows your application to handle the intent. If not, you must request permission explicitly in your application before connecting to the device. |
Explicitly asking for permission might be necessary in some situations such as when your application enumerates USB devices that are already connected and then wants to communicate with one.
To explicitly obtain permission:
Call UsbManager.requestPermission().
The call to requestPermission() displays a dialog to the user asking for permission to connect to the device.
The system generates a broadcast intent with a boolean EXTRA_PERMISSION_GRANTED extra indicating the user’s response.
The following sample code shows how to create a broadcast receiver to process the response:
private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"; private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ACTION_USB_PERMISSION.equals(action)) { synchronized (this) { UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { if (device != null) { // Call method to set up device communication } } else { Log.d(TAG, "permission denied for device " + device); } } } } };
To register the broadcast receiver, add this in your onCreate() method in your activity:
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); registerReceiver(mUsbReceiver, filter);
To display the dialog that asks users for permission to connect to the device, call the UsbManager.requestPermission() method:
UsbManager mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE); UsbDevice device; // ... mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0); mUsbManager.requestPermission(device, mPermissionIntent);
Communication with a USB device can be either synchronous or asynchronous.
To properly set up communication with a device, you need to obtain the appropriate UsbInterface and UsbEndpoint of the device that you want to communicate on and send requests on this endpoint with a UsbDeviceConnection. In general, your code should:
The following code snippet is a trivial way to do a synchronous data transfer. Your code should have more logic to correctly find the correct interface and endpoints to communicate on and also should do any transferring of data in a different thread than the main UI thread:
private byte[] bytes = "hello usb".getBytes(); private static int TIMEOUT = 0; private boolean forceClaim = true; ... UsbInterface intf = device.getInterface(0); UsbEndpoint endpoint = intf.getEndpoint(0); UsbDeviceConnection connection = mUsbManager.openDevice(device); connection.claimInterface(intf, forceClaim); connection.bulkTransfer(endpoint, bytes, bytes.length, TIMEOUT); // Do in another thread
To send data asynchronously, use the UsbRequest class to initialize and queue an asynchronous request, then wait for the result with requestWait().
When you are done communicating with a device or if the device was detached, close the UsbInterface and UsbDeviceConnection by calling releaseInterface() and close().
To listen for detached events, create a broadcast receiver like below:
BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (device != null) { // call your method that cleans up and closes communication with the device } } } };
Creating the broadcast receiver within the application, and not the manifest, allows your application to handle detached events only while it is running.
The USB accessory APIs were introduced to the platform in Android 3.1. They are also available in Android 2.3.4 using the Google APIs add-on library.
To support USB accessory mode in Android 2.3.4, the Google APIs add-on library includes the backported USB accessory APIs and they are contained in this namespace.
This namespace contains the classes that support USB accessory mode in Android 3.1.
The following classes support the USB accessory APIs:
Allows you to enumerate and communicate with connected USB accessories.
UsbManager manager = UsbManager.getInstance(this);
UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
Represents a USB accessory and contains methods to access its identifying information.
UsbAccessory accessory = UsbManager.getAccessory(intent);
UsbAccessory accessory = (UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
Because the add-on library is a wrapper for the framework APIs, the classes that support the USB accessory feature are similar. You can use the reference documentation for the android.hardware.usb even if you are using the add-on library.
Not all Android-powered devices are guaranteed to support the USB accessory APIs.
To indicate that your application requires USB accessory support, add the following elements to your application’s manifest:
<uses-feature android:name="android.hardware.usb.accessory" />
<uses-sdk android:minSdkVersion="12" />
<uses-library android:name="com.android.future.usb.accessory" />
When users connect USB accessories to an Android-powered device, the Android system can determine whether your application is interested in the connected accessory. If so, you can set up communication with the accessory if desired.
To do this, your application has to:
Discover connected accessories by using an intent filter that filters for accessory attached events or by enumerating connected accessories and finding the appropriate one.
Ask the user for permission to communicate with the accessory, if not already obtained.
Communicate with the accessory by reading and writing data on the appropriate interface endpoints.
To have your application discover a particular USB device, you can specify an intent filter to filter for the android.hardware.usb.action.USB_ACCESSORY_ATTACHED intent.
In your activity, you can obtain the UsbAccessory that represents the attached device from the intent.
UsbAccessory accessory = UsbManager.getAccessory(intent);
UsbAccessory accessory = (UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
The following example shows how to declare the intent filter in your application’s manifest:
<activity ...> ... <intent-filter> <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" /> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" android:resource="@xml/accessory_filter" /> </activity>
The <meta-data> element points to an external XML resource file that declares identifying information about the device that you want to detect.
In the XML resource file, declare <usb-accessory> elements for the accessories that you want to filter.
<?xml version="1.0" encoding="utf-8"?> <resources> <usb-accessory model="DemoKit" manufacturer="Google" version="1.0"/> </resources>
Save the resource file in your application’s res/xml/ directory.
You can have your application enumerate accessories that have identified themselves while your application is running.
Use the UsbManager.getAccessoryList() method to get an array all the USB accessories that are connected:
UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE); UsbAccessory[] accessoryList = manager.getAcccessoryList();
|
|
Currently, only one connected accessory is supported at one time, but the API is designed to support multiple accessories in the future. |
Before communicating with the USB accessory, your application must have permission from your users.
|
|
If your application uses an intent filter to discover USB devices as they’re connected, it automatically receives permission if the user allows your application to handle the intent. If not, you must request permission explicitly in your application before connecting to the device. |
Explicitly asking for permission might be necessary in some situations such as when your application enumerates USB devices that are already connected and then wants to communicate with one.
To explicitly obtain permission:
Call UsbManager.requestPermission().
The call to requestPermission() displays a dialog to the user asking for permission to connect to the accessory.
The system generates a broadcast intent with a boolean EXTRA_PERMISSION_GRANTED extra indicating the user’s response.
The following sample code shows how to create a broadcast receiver to process the response:
private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"; private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ACTION_USB_PERMISSION.equals(action)) { synchronized (this) { UsbAccessory accessory = (UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY); if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { if (accessory != null) { // Call method to set up accessory communication } } else { Log.d(TAG, "permission denied for accessory " + accessory); } } } } };
To register the broadcast receiver, add this in your onCreate() method in your activity:
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); registerReceiver(mUsbReceiver, filter);
To display the dialog that asks users for permission to connect to the device, call the UsbManager.requestPermission() method:
UsbManager mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE); UsbAccessory accessory; // ... mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0); mUsbManager.requestPermission(accessory, mPermissionIntent);
You can communicate with the accessory by using the UsbManager to obtain a file descriptor that you can set up input and output streams to read and write data to descriptor.
UsbAccessory mAccessory; ParcelFileDescriptor mFileDescriptor; FileInputStream mInputStream; FileOutputStream mOutputStream; // ... private void openAccessory() { Log.d(TAG, "openAccessory: " + accessory); mFileDescriptor = mUsbManager.openAccessory(mAccessory); if (mFileDescriptor != null) { FileDescriptor fd = mFileDescriptor.getFileDescriptor(); mInputStream = new FileInputStream(fd); mOutputStream = new FileOutputStream(fd); Thread thread = new Thread(null, this, "AccessoryThread"); thread.start(); } }
In the thread’s run() method, you can read and write to the accessory by using the FileInputStream or FileOutputStream objects.
When reading data from an accessory with a FileInputStream object, ensure that the buffer that you use is big enough to store the USB packet data.
|
|
At a lower level, the packets are 64 bytes for USB full-speed accessories and 512 bytes for USB high-speed accessories. The Android accessory protocol bundles the packets together for both speeds into one logical packet for simplicity. |
When you are done communicating with an accessory or if the accessory was detached, close the file descriptor that you opened by calling close().
To listen for detached events, create a broadcast receiver like below:
BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) { UsbAccessory accessory = (UsbAccessory)intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY); if (accessory != null) { // call your method that cleans up and closes communication with the accessory } } } };
Creating the broadcast receiver within the application, and not the manifest, allows your application to handle detached events only while it is running.
The Android Open Accessory Development Kit (ADK) provides an implementation of an Android USB accessory that is based on the Arduino open source electronics prototyping platform.
The main hardware and software components of the ADK include:
In this module, you learned about support for USB in Android. We explored how USB works, how to use USB Host mode, and how to use USB Accessories.
Android is a complex system, composed of many moving parts and layers. In this module, we will explore Android’s many tools and configuration options that will enable us to:
$ adb shell dmesg …init: cannot find '/system/etc/install-recovery.sh', disabling 'flash_recovery'
eth0: link up
warning: `rild' uses 32-bit capabilities (legacy support in use)
eth0: no IPv6 routers present
init: sys_prop: permission denied uid:1003 name:service.bootanim.exit
binder: 404:414 refcount change on invalid ref 14
binder: 404:414 refcount change on invalid ref 25 …
|
|
To clear the contents of this buffer, we can run adb shell dmesg -c |
|
|
We can get messages to also include timestamps, but we first need to enable this via a kernel option (see below). |
The kernel message buffer is the destination of kernel’s printk(…) function. This function is the primary way driver developers log debugging messages that the userspace can see.
This buffer is fixed in size, and oldest messages automatically expire when the buffer gets full. The size of the buffer defaults to 64KB, and is configurable via CONFIG_LOG_BUF_SHIFT kernel’s configuration option. The actual size is set to 1 << CONFIG_LOG_BUF_SHIFT (i.e. 2 ^ CONFIG_LOG_BUF_SHIFT).
$ $ANDROID_HOST_OUT/bin/emulator -kernel path/to/kernel -show-kernel & … init: cannot find '/system/etc/install-recovery.sh', disabling 'flash_recovery' $ eth0: link up warning: `rild' uses 32-bit capabilities (legacy support in use) init: sys_prop: permission denied uid:1003 name:service.bootanim.exit binder: 404:414 refcount change on invalid ref 14 binder: 404:414 refcount change on invalid ref 25 …
|
|
Using -verbose command-line option will get the emulator to display debug information about itself. |
$ make menuconfig ARCH=arm
$ adb logcat -b events …
|
|
If we don’t specify -b switch, LogCat reads from main by default. |
$ adb logcat -t 5 -v long --------- beginning of /dev/log/system [ 10-01 22:55:46.132 307: 467 W/ThrottleService ] unable to find stats for iface rmnet0 [ 10-01 23:05:46.132 307: 467 W/ThrottleService ] unable to find stats for iface rmnet0 [ 10-01 23:05:46.140 120: 1808 W/SocketClient ] write error (Broken pipe) [ 10-01 23:15:46.140 307: 467 W/ThrottleService ] unable to find stats for iface rmnet0 --------- beginning of /dev/log/main [ 10-01 23:17:00.085 307: 309 D/dalvikvm ] GC_CONCURRENT freed 988K, 8% free 15035K/16199K, paused 19ms+6ms, total 84ms
|
|
The -t <count> switch tells logcat to print only the most recent <count> lines. |
$ adb logcat -d "ActivityManager:I" "*:S"
--------- beginning of /dev/log/system
I/ActivityManager( 307): Memory class: 64
I/ActivityManager( 307): Enabled StrictMode logging for AThread's Looper
I/ActivityManager( 307): Config changed: {1.0 0mcc0mnc en_US sw360dp w360dp h567dp nrml port ?uimode ?night -touch -keyb/v/h -nav/h s.2}
I/ActivityManager( 307): Config changed: {1.0 0mcc0mnc en_US sw360dp w360dp h567dp nrml port ?uimode ?night finger -keyb/v/h -nav/h s.3}
I/ActivityManager( 307): System now ready
…
--------- beginning of /dev/log/main
|
|
The filter spec take the following format <tag>[:priority] where priority is one of Verbose, Debug, Info, Warn, Error, Fatal, and Silent. |
|
|
The -d switch tells logcat to print what’s available the log buffer and then exit (i.e. not block). |
$ adb logcat -g -b events /dev/log/events: ring buffer is 256Kb (17Kb consumed), max entry is 5120b, max payload is 4076b
$ adb logcat -b main -c $ adb logcat -b main -g /dev/log/main: ring buffer is 256Kb (0Kb consumed), max entry is 5120b, max payload is 4076b
… public final class ActivityManagerService extends ActivityManagerNative implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback { private static final String USER_DATA_DIR = "/data/user/"; static final String TAG = "ActivityManager"; static final String TAG_MU = "ActivityManagerServiceMU"; static final boolean DEBUG = false; static final boolean localLOGV = DEBUG; static final boolean DEBUG_SWITCH = localLOGV || false; static final boolean DEBUG_TASKS = localLOGV || false; static final boolean DEBUG_PAUSE = localLOGV || false; static final boolean DEBUG_OOM_ADJ = localLOGV || false; static final boolean DEBUG_TRANSITION = localLOGV || false; static final boolean DEBUG_BROADCAST = localLOGV || false; static final boolean DEBUG_BACKGROUND_BROADCAST = DEBUG_BROADCAST || false; static final boolean DEBUG_BROADCAST_LIGHT = DEBUG_BROADCAST || false; static final boolean DEBUG_SERVICE = localLOGV || false; static final boolean DEBUG_SERVICE_EXECUTING = localLOGV || false; static final boolean DEBUG_VISBILITY = localLOGV || false; static final boolean DEBUG_PROCESSES = localLOGV || false; static final boolean DEBUG_PROCESS_OBSERVERS = localLOGV || false; static final boolean DEBUG_PROVIDER = localLOGV || false; static final boolean DEBUG_URI_PERMISSION = localLOGV || false; static final boolean DEBUG_USER_LEAVING = localLOGV || false; static final boolean DEBUG_RESULTS = localLOGV || false; static final boolean DEBUG_BACKUP = localLOGV || false; static final boolean DEBUG_CONFIGURATION = localLOGV || false; static final boolean DEBUG_POWER = localLOGV || false; static final boolean DEBUG_POWER_QUICK = DEBUG_POWER || false; static final boolean DEBUG_MU = localLOGV || false; static final boolean VALIDATE_TOKENS = false; static final boolean SHOW_ACTIVITY_START_TIME = true; … private final boolean updateOomAdjLocked( ProcessRecord app, int hiddenAdj, ProcessRecord TOP_APP, boolean doingAll) { … if (app.curAdj != app.setAdj) { if (Process.setOomAdj(app.pid, app.curAdj)) { if (DEBUG_SWITCH || DEBUG_OOM_ADJ) Slog.v( TAG, "Set " + app.pid + " " + app.processName + " adj " + app.curAdj + ": " + app.adjType); app.setAdj = app.curAdj; } else { success = false; Slog.w(TAG, "Failed setting oom adj of " + app + " to " + app.curAdj); } } … } … }
$ adb shell 'echo "" > /data/anr/traces.txt' $ adb shell kill -3 $(pid com.android.systemui) $ adb shell cat /data/anr/traces.txt ----- pid 390 at 2012-10-02 01:55:21 ----- Cmd line: com.android.systemui DALVIK THREADS: (mutexes: tll=0 tsl=0 tscl=0 ghl=0) "main" prio=5 tid=1 NATIVE | group="main" sCount=1 dsCount=0 obj=0x40edd568 self=0x40e00ab0 | sysTid=390 nice=0 sched=0/0 cgrp=apps handle=1074923056 | schedstat=( 0 0 0 ) utm=194 stm=64 core=0 #00 pc 0000da40 /system/lib/libc.so (epoll_wait+12) #01 pc 00014791 /system/lib/libutils.so (android::Looper::pollInner(int)+96) #02 pc 000149f9 /system/lib/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+104) #03 pc 0005d11b /system/lib/libandroid_runtime.so (android::NativeMessageQueue::pollOnce(_JNIEnv*, int)+22) #04 pc 0001de30 /system/lib/libdvm.so (dvmPlatformInvoke+112) #05 pc 0004d083 /system/lib/libdvm.so (dvmCallJNIMethod(unsigned int const*, JValue*, Method const*, Thread*)+394) #06 pc 00027260 /system/lib/libdvm.so #07 pc 0002bb68 /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+180) #08 pc 0005fab7 /system/lib/libdvm.so (dvmInvokeMethod(Object*, Method const*, ArrayObject*, ArrayObject*, ClassObject*, bool)+374) #09 pc 0006700d /system/lib/libdvm.so #10 pc 00027260 /system/lib/libdvm.so #11 pc 0002bb68 /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+180) #12 pc 0005f7f1 /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list)+272) #13 pc 00049673 /system/lib/libdvm.so #14 pc 0004698d /system/lib/libandroid_runtime.so #15 pc 0004746f /system/lib/libandroid_runtime.so (android::AndroidRuntime::start(char const*, char const*)+390) #16 pc 00000dcf /system/bin/app_process #17 pc 00016ed3 /system/lib/libc.so (__libc_init+38) #18 pc 00000b34 /system/bin/app_process at android.os.MessageQueue.nativePollOnce(Native Method) at android.os.MessageQueue.next(MessageQueue.java:125) at android.os.Looper.loop(Looper.java:124) at android.app.ActivityThread.main(ActivityThread.java:4745) at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke(Method.java:511) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553) at dalvik.system.NativeStart.main(Native Method) … "Binder_3" prio=5 tid=11 NATIVE | group="main" sCount=1 dsCount=0 obj=0x41957ce8 self=0x5830b008 | sysTid=411 nice=0 sched=0/0 cgrp=apps handle=1542627192 | schedstat=( 0 0 0 ) utm=3 stm=0 core=0 #00 pc 0000cb60 /system/lib/libc.so (__ioctl+8) #01 pc 00027d75 /system/lib/libc.so (ioctl+16) #02 pc 00016bfd /system/lib/libbinder.so (android::IPCThreadState::talkWithDriver(bool)+124) #03 pc 000173af /system/lib/libbinder.so (android::IPCThreadState::joinThreadPool(bool)+154) #04 pc 0001b171 /system/lib/libbinder.so #05 pc 00010f47 /system/lib/libutils.so (android::Thread::_threadLoop(void*)+114) #06 pc 000468bb /system/lib/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell(void*)+66) #07 pc 00010aad /system/lib/libutils.so #08 pc 00012bb0 /system/lib/libc.so (__thread_entry+48) #09 pc 00012308 /system/lib/libc.so (pthread_create+172) at dalvik.system.NativeStart.run(Native Method) …
$ systemstack $ adb shell cat /data/anr/traces.txt
|
|
This basically does the following: $ adb shell echo '""' '>>' /data/anr/traces.txt $ adb shell chmod 776 /data/anr/traces.txt & adb shell kill -3 $(pid system_server) |
Map a port on our host machine to a port on the emulator or device that GDB will listen on:
$ adb forward tcp:5039 tcp:5039
Here, we use port 5039 because it is what gdbclient helper function uses (provided by build/envsetup.sh), although the "official" port for GDB is 2159, according IANA.
When this is done, we’ll have 127.0.0.1:5039 on our host machine → 0.0.0.0:5039 on the emulator/device.
Emulator also permits port-mapping via its management console (assuming it’s accessible via 5554):
$ echo "redir add tcp:5039:5039" | nc localhost 5554
Debug a new program or an existing process with the help of the GDB server:
$ adb shell gdbserver :5039 /system/bin/mrknlog &
|
|
Here, we are starting a (custom) program (mrknlog) that prints the used/total size of /dev/log/main buffer and flushes it before exiting. |
$ pid mrknlogd 47 $ adb shell gdbserver :5039 --attach 47 & Attached; pid = 47 Listening on port 5039
|
|
Here, we are connecting to a (custom) daemon process (mrknlogd), which does exactly what mrknlog does, except repeatedly, while sleeping between executions. We assume mrknlogd is already running (e.g. started by init). |
|
|
The shell function pid mrknlogd (provided by build/envsetup.sh) is equivalent to: $ adb shell ps | grep mrknlogd | awk '{print $2}'
47
|
Get the location of unstripped binaries:
$ export SYMBOLS=$ANDROID_PRODUCT_OUT/symbols
|
|
Here, ANDROID_PRODUCT_OUT is set by the lunch command. Alternatively, we could also do: $ export SYMBOLS=`get_abs_build_var TARGET_OUT_UNSTRIPPED` |
To save on (flash-memory) space, Android by default strips all of the symbols from its executables and shared libraries. While symbols (i.e. the debug info) are not needed at run-time, GDB uses the symbol table to resolve line numbers as well as function and variable names. We’d be lost trying to debug without these.
So, what do we do?
We could provide GDB with location of unstripped binaries (the direction we’ll take).
Or, we could re-compile our binaries and instruct the build system not to strip the symbol information (as well as disable compiler optimizations), by placing the following in our Android.mk file(s):
LOCAL_CFLAGS += -g -O0
Replace LOCAL_CFLAGS with LOCAL_CPPFLAGS in case of C++.
Connect with the debugger client (gdb) to the debugger server (gdbserver):
$ANDROID_TOOLCHAIN/arm-linux-androideabi-gdb \ -ex "set solib-absolute-prefix $SYMBOLS" \ -ex "set solib-search-path $SYMBOLS/system/lib:$SYMBOLS/system/lib/hw:$SYMBOLS/system/lib/ssl/engines" \ -ex "target remote :5039" \ --quiet \ $SYMBOLS/system/bin/mrknlog Reading symbols from /home/student/aosp/out/target/product/alpha/symbols/system/bin/mrknlog...done. Remote debugging using :5039 Remote debugging from host 127.0.0.1 __dl__start () at bionic/linker/arch/arm/begin.S:35 35 mov r0, sp (gdb)
|
|
For x86 targets, run $ANDROID_EABI_TOOLCHAIN/i686-android-linux-gdb instead. Replace mrknlog with mrknlogd when attaching to our mrknlogd daemon. |
|
|
Android ships with a gdbclient bash function (courtesy of build/envsetup.sh) that tries to automate all of these steps, though its usage is somewhat confusing. |
|
|
You may find GDB cheatsheet handy. |
Here we are running gdb client from the Android-provided arch-specific toolchain.
Using the -ex command-line options, we are asking gdb to:
Initialize the system root to the root of our un-stripped binaries
Initialize the search path for shared objects to the locations of our unstripped shared libraries
Connect to the GDB server on the previously mapped port (127.0.0.1:5039)
Finally, we are also providing gdb with the unstripped version of the executable we are debugging.
Start debugging:
(gdb) break main
Breakpoint 1 at 0x2a0004e0: file device/marakana/alpha/bin/mrknlog/mrknlog.c, line 7.
(gdb) continue
Continuing.
Breakpoint 1, main (argc=1, argv=0xbee96cb4) at device/marakana/alpha/bin/mrknlog/mrknlog.c:7
7 int main (int argc, char* argv[]) {
(gdb) list
2 #include <string.h>
3 #include <errno.h>
4
5 #include <mrknlog.h>
6
7 int main (int argc, char* argv[]) {
8 int usedSize = mrkn_get_used_log_size();
9 int totalSize = mrkn_get_total_log_size();
10 if (totalSize >= 0 && usedSize >= 0) {
11 if (mrkn_flush_log() == 0) {
(gdb) b 11
Breakpoint 2 at 0x2a0004fe: file device/marakana/alpha/bin/mrknlog/mrknlog.c, line 11.
(gdb) c
Continuing.
Breakpoint 2, main (argc=<optimized out>, argv=<optimized out>)
at device/marakana/alpha/bin/mrknlog/mrknlog.c:11
11 if (mrkn_flush_log() == 0) {
(gdb) print usedSize
$1 = 223
(gdb) bt
#0 main (argc=<optimized out>, argv=<optimized out>) at device/marakana/alpha/bin/mrknlog/mrknlog.c:11
…
…
Reading symbols from /home/student/aosp/out/target/product/alpha/symbols/system/bin/mrknlogd...done.
Remote debugging using :5039
Remote debugging from host 127.0.0.1
warning: .dynamic section for "/home/student/aosp/out/target/product/alpha/symbols/system/bin/linker" is not at the expected address (wrong library or version mismatch?)
nanosleep () at bionic/libc/arch-arm/syscalls/nanosleep.S:10
10 ldmfd sp!, {r4, r7}
(gdb) bt
#0 nanosleep () at bionic/libc/arch-arm/syscalls/nanosleep.S:10
#1 0x40043294 in sleep (seconds=<optimized out>) at bionic/libc/unistd/sleep.c:45
#2 0x2a0005e0 in main (argc=<optimized out>, argv=<optimized out>)
at device/marakana/alpha/bin/mrknlogd/mrknlogd.c:28
(gdb) frame 2
#2 0x2a0005e0 in main (argc=<optimized out>, argv=<optimized out>)
at device/marakana/alpha/bin/mrknlogd/mrknlogd.c:28
28 sleep(frequency);
(gdb) print frequency
$1 = 60
(gdb) set frequency=10
(gdb) print frequency
$2 = 60
(gdb) break 27
Breakpoint 1 at 0x2a0005da: file device/marakana/alpha/bin/mrknlogd/mrknlogd.c, line 27.
(gdb) c
Continuing.
Breakpoint 1, main (argc=<optimized out>, argv=<optimized out>)
at device/marakana/alpha/bin/mrknlogd/mrknlogd.c:28
28 sleep(frequency);
(gdb) print usedSize
$6 = 415
(gdb) detach
Detaching from process 47
Ending remote debugging.
(gdb) quit
$
$ adb shell ps | grep system.*com.marakana.android.logservice | awk '{print $2}'
300
$ adb shell gdbserver :5039 --attach 300 &
Attached; pid = 300
Listening on port 5039
$ $ANDROID_TOOLCHAIN/arm-linux-androideabi-gdb \
-ex "set solib-absolute-prefix $SYMBOLS" \
-ex "set solib-search-path $SYMBOLS/system/lib:$SYMBOLS/system/lib/hw:$SYMBOLS/system/lib/ssl/engines" \
-ex "set breakpoint pending on" \
-ex "target remote :5039" \
--quiet \
$SYMBOLS/system/bin/app_process
Reading symbols from /home/student/aosp/out/target/product/alpha/symbols/system/bin/app_process...done.
Remote debugging using :5039
Remote debugging from host 127.0.0.1
warning: .dynamic section for "/home/student/aosp/out/target/product/alpha/symbols/system/bin/linker" is not at the expected address (wrong library or version mismatch?)
epoll_wait () at bionic/libc/arch-arm/syscalls/epoll_wait.S:10
10 ldmfd sp!, {r4, r7}
(gdb) b device/marakana/alpha/lib/libmrknlog/libmrknlog.c:22
Breakpoint 1 at 0x49f96504: file device/marakana/alpha/lib/libmrknlog/libmrknlog.c, line 22.
(gdb) c
Continuing.
[New Thread 1171]
[Switching to Thread 1171]
Breakpoint 1, ioctl_log (mode=<optimized out>, request=44546)
at device/marakana/alpha/lib/libmrknlog/libmrknlog.c:24
24 }
(gdb) print request
$1 = 44546
(gdb) detach
(gdb) quit
Debugging native-code in any Dalvik-based (or Zygote-forked) process requires that we start from system/bin/app_process, because that’s what zygote first runs. In order to reference code in shared libraries (yet to be loaded), we need enable pending break points.
We can either do this via -ex "set breakpoint pending on" command-line switch, or in GDB when it starts:
(gdb) set breakpoint pending on
Debugging Java code will be covered later.
$ pid system_server
307
$ adb shell gdbserver :5039 --attach 307 &
Attached; pid = 307
Listening on port 5039
$ $ANDROID_TOOLCHAIN/arm-linux-androideabi-gdb \
-ex "set solib-absolute-prefix $SYMBOLS" \
-ex "set solib-search-path $SYMBOLS/system/lib:$SYMBOLS/system/lib/hw:$SYMBOLS/system/lib/ssl/engines:$SYMBOLS/system/vendor/lib/hw:$ANDROID_PRODUCT_OUT/obj/lib" \
-ex "set breakpoint pending on" \
-ex "target remote :5039" \
--quiet \
$SYMBOLS/system/bin/app_process
Reading symbols from /Volumes/Android/aosp-4.1/out/target/product/maguro/symbols/system/bin/app_process...done.
Remote debugging using :5039
Remote debugging from host 127.0.0.1
warning: .dynamic section for "/Volumes/Android/aosp-4.1/out/target/product/maguro/symbols/system/bin/linker" is not at the expected address (wrong library or version mismatch?)
__ioctl () at bionic/libc/arch-arm/syscalls/__ioctl.S:9
9 swi #0
(gdb) break hardware/libhardware_legacy/vibrator/vibrator.c:61
Breakpoint 1 at 0x400ecc3c: file hardware/libhardware_legacy/vibrator/vibrator.c, line 61.
(gdb) c
Continuing.
Breakpoint 1, sendit (timeout_ms=20) at hardware/libhardware_legacy/vibrator/vibrator.c:61
61 close(fd);
(gdb) bt
#0 sendit (timeout_ms=20) at hardware/libhardware_legacy/vibrator/vibrator.c:61
#1 0x400ecc8c in vibrator_on (timeout_ms=20) at hardware/libhardware_legacy/vibrator/vibrator.c:69
#2 0x56f90d3c in android::vibratorOn (env=0x5c514d30, clazz=0x40200009, timeout_ms=20) at frameworks/base/services/jni/com_android_server_VibratorService.cpp:40
#3 0x4073de34 in dvmPlatformInvoke () at dalvik/vm/arch/arm/CallEABI.S:258
#4 0x4076d086 in dvmCallJNIMethod (args=0x5bbe1794, pResult=0x58000400, method=0x572856d0, self=0x580003f0) at dalvik/vm/Jni.cpp:1155
…
#23 0x407743d6 in interpThreadStart (arg=0x580003f0) at dalvik/vm/Thread.cpp:1538
#24 0x40029bb4 in __thread_entry (func=0x40774335 <interpThreadStart(void*)>, arg=0x580003f0, tls=<optimized out>) at bionic/libc/bionic/pthread.c:217
#25 0x4002930c in pthread_create (thread_out=0x5c3f1e28, attr=0x5e2bed70, start_routine=0x40774335 <interpThreadStart(void*)>, arg=0x580003f0) at bionic/libc/bionic/pthread.c:356
#26 0x00000000 in ?? ()
(gdb) frame 2
#2 0x56f90d3c in android::vibratorOn (env=0x5c514d30, clazz=0x40200009, timeout_ms=20) at frameworks/base/services/jni/com_android_server_VibratorService.cpp:40
40 vibrator_on(timeout_ms);
(gdb) list
35 }
36
37 static void vibratorOn(JNIEnv *env, jobject clazz, jlong timeout_ms)
38 {
39 // ALOGI("vibratorOn\n");
40 vibrator_on(timeout_ms);
41 }
42
43 static void vibratorOff(JNIEnv *env, jobject clazz)
44 {
(gdb) print timeout_ms
$1 = 20
(gdb) detach
(gdb) quit
|
|
In this case, we disabled:
Otherwise, we would have had harder time walking through the code we were interested in. |
When debugging on real hardware devices, we should also include proprietary binaries (mostly HAL libraries) into our solib-search-path.
These binaries are not under $ANDROID_PRODUCT_OUT/symbols/*, because they come pre-built.
All we need to do is append $SYMBOLS/system/vendor/lib/hw:$ANDROID_PRODUCT_OUT/obj/lib to solib-search-path.
Most processes on Android are multi-threaded, but GDB can only step through one thread at a time. In order to prevent (suspend) threads from running while stepping through code, we can:
(gdb) set scheduler-locking on
We just have to remember to either continue or disable the lock, in order to allow for normal execution. Otherwise, we could cause a dead-lock.
(gdb) set scheduler-locking off
For more info, see Debugging Programs with Multiple Threads
<manifest …> … <application … android:debuggable="true"> … </application> </manifest>
|
|
Setting an application as debuggable enables it to be debugged even when running on a device in user mode. Be sure to set android:debuggable="false" before you build for release, as published applications should usually not be debuggable. |
Add (toggle) breakpoints directly in the code editor in the horizontal bar next to the line numbers
Select the root of the project
Select Menu → Run → Debug As → Android Application
Switch to Debug perspective (this should happen automatically) via Menu → Window → Open Perspective → Debug
Examine the variables, step through the code, manage break-points, etc.
$ adb jdwp 316 398 445 …
$ adb shell ps -t |grep JDWP system 322 316 556492 68024 c054da44 4010772c S JDWP u0_a37 403 398 489208 61740 c015fa1c 40106c88 S JDWP u0_a11 449 445 467944 36636 c054da44 4010772c S JDWP …
$ adb forward tcp:8700 jdwp:398
|
|
In this case, DDMS acts as a tunnel that roughly looks like this: JDWP Debugger → DDMS (port 8700) → ADB Host Daemon → USB/TCP → adbd → Dalvik VM (PID 850) |
$ jdb -attach 127.0.0.1:8700 Set uncaught java.lang.Throwable Set deferred uncaught java.lang.Throwable Initializing jdb ... >
|
|
See jdb - The Java Debugger for more info on how to use it |
Go to Java Perspective (Menu → Window → Open Perspective → Java)
Go to Menu → Run → Debug Configurations… → Remote Java Application → New (icon)
Under the Connect tab, go to Connection Properties select port 8700
Under the Source tab, Add… → File System Directory and add path(s) to Java source directories (that you wish to debug)
After pressing OK (to dismiss the Add Source dialog), hit the Debug button
Switch to Debug perspective
Examine the variables, step through the code, manage break-points, etc.
$ adb forward tcp:8700 jdwp:$(pid system_server) jdb -attach 127.0.0.1:8700 Set uncaught java.lang.Throwable Set deferred uncaught java.lang.Throwable Initializing jdb ... > stop in com.android.server.am.ActivityManagerService.updateOomAdjLocked() Set breakpoint com.android.server.am.ActivityManagerService.updateOomAdjLocked() > stop in com.android.server.am.ActivityManagerService.updateOomAdjLocked( Breakpoint hit: "thread=android.server.ServerThread", com.android.server.am.ActivityManagerService.updateOomAdjLocked(), line=14,705 bci=0 > stop at com.android.server.am.ActivityManagerService:14731 Set breakpoint com.android.server.am.ActivityManagerService:14731
android.server.ServerThread[1] cont
android.server.ServerThread[1] where [1] com.android.server.am.ActivityManagerService.updateOomAdjLocked (ActivityManagerService.java:14,731) [2] com.android.server.am.BroadcastQueue.deliverToRegisteredReceiverLocked (BroadcastQueue.java:418) [3] com.android.server.am.BroadcastQueue.processNextBroadcast (BroadcastQueue.java:634) [4] com.android.server.am.ActivityManagerService.finishReceiver (ActivityManagerService.java:13,281) [5] android.content.BroadcastReceiver$PendingResult.sendFinished (BroadcastReceiver.java:417) [6] android.content.BroadcastReceiver$PendingResult.finish (BroadcastReceiver.java:393) [7] android.app.LoadedApk$ReceiverDispatcher$Args.run (LoadedApk.java:772) [8] android.os.Handler.handleCallback (Handler.java:615) [9] android.os.Handler.dispatchMessage (Handler.java:92) [10] android.os.Looper.loop (Looper.java:137) [11] com.android.server.ServerThread.run (SystemServer.java:875)
android.server.ServerThread[1] print numSlots numSlots = 7
android.server.ServerThread[1] print factor factor = 2
android.server.ServerThread[1] print i i = 21
android.server.ServerThread[1] quit
|
|
Recall that pid is a function exposed by build/envsetup.sh. |
$ adb shell service list Found 70 services: 0 sip: [android.net.sip.ISipService] 1 phone: [com.android.internal.telephony.ITelephony] 2 iphonesubinfo: [com.android.internal.telephony.IPhoneSubInfo] 3 simphonebook: [com.android.internal.telephony.IIccPhoneBook] 4 isms: [com.android.internal.telephony.ISms] 5 nfc: [android.nfc.INfcAdapter] 6 com.marakana.android.service.log.ILogService: [com.marakana.android.service.log.ILogService] 7 commontime_management: [] 8 samplingprofiler: [] 9 diskstats: [] 10 appwidget: [com.android.internal.appwidget.IAppWidgetService] 11 backup: [android.app.backup.IBackupManager] 12 uimode: [android.app.IUiModeManager] 13 serial: [android.hardware.ISerialManager] 14 usb: [android.hardware.usb.IUsbManager] 15 audio: [android.media.IAudioService] 16 wallpaper: [android.app.IWallpaperManager] 17 dropbox: [com.android.internal.os.IDropBoxManagerService] 18 search: [android.app.ISearchManager] 19 country_detector: [android.location.ICountryDetector] 20 location: [android.location.ILocationManager] 21 devicestoragemonitor: [] 22 notification: [android.app.INotificationManager] 23 updatelock: [android.os.IUpdateLock] 24 throttle: [android.net.IThrottleManager] 25 servicediscovery: [android.net.nsd.INsdManager] 26 connectivity: [android.net.IConnectivityManager] 27 wifi: [android.net.wifi.IWifiManager] 28 wifip2p: [android.net.wifi.p2p.IWifiP2pManager] 29 netpolicy: [android.net.INetworkPolicyManager] 30 netstats: [android.net.INetworkStatsService] 31 textservices: [com.android.internal.textservice.ITextServicesManager] 32 network_management: [android.os.INetworkManagementService] 33 clipboard: [android.content.IClipboard] 34 statusbar: [com.android.internal.statusbar.IStatusBarService] 35 device_policy: [android.app.admin.IDevicePolicyManager] 36 lock_settings: [com.android.internal.widget.ILockSettings] 37 mount: [IMountService] 38 accessibility: [android.view.accessibility.IAccessibilityManager] 39 input_method: [com.android.internal.view.IInputMethodManager] 40 bluetooth_a2dp: [android.bluetooth.IBluetoothA2dp] 41 bluetooth: [android.bluetooth.IBluetooth] 42 input: [android.hardware.input.IInputManager] 43 window: [android.view.IWindowManager] 44 alarm: [android.app.IAlarmManager] 45 vibrator: [android.os.IVibratorService] 46 battery: [] 47 hardware: [android.os.IHardwareService] 48 content: [android.content.IContentService] 49 account: [android.accounts.IAccountManager] 50 permission: [android.os.IPermissionController] 51 cpuinfo: [] 52 dbinfo: [] 53 gfxinfo: [] 54 meminfo: [] 55 activity: [android.app.IActivityManager] 56 package: [android.content.pm.IPackageManager] 57 media.audio_policy: [android.media.IAudioPolicyService] 58 scheduling_policy: [android.os.ISchedulingPolicyService] 59 telephony.registry: [com.android.internal.telephony.ITelephonyRegistry] 60 usagestats: [com.android.internal.app.IUsageStats] 61 batteryinfo: [com.android.internal.app.IBatteryStats] 62 power: [android.os.IPowerManager] 63 entropy: [] 64 sensorservice: [android.gui.SensorServer] 65 SurfaceFlinger: [android.ui.ISurfaceComposer] 66 media.camera: [android.hardware.ICameraService] 67 media.player: [android.media.IMediaPlayerService] 68 media.audio_flinger: [android.media.IAudioFlinger] 69 drm.drmManager: [drm.IDrmManagerService]
$ adb shell service check activity Service activity: found $ adb shell service check foo Service foo: not found
$ adb shell service call com.marakana.android.service.log.ILogService 1 Result: Parcel(00000000 '....') $ adb shell service call com.marakana.android.service.log.ILogService 2 Result: Parcel(00000000 00040000 '........') $ adb shell service call com.marakana.android.service.log.ILogService 3 Result: Parcel(00000000 00000000 '........') $ adb shell log test $ adb shell service call com.marakana.android.service.log.ILogService 3 Result: Parcel(00000000 00000023 '....#...')
|
|
The reply values are expressed in hexadecimal notation. |
$ adb shell dumpsys meminfo
Applications Memory Usage (kB):
Uptime: 482138 Realtime: 482126
Total PSS by process:
44415 kB: com.android.launcher (pid 519)
33696 kB: system (pid 307)
28349 kB: com.android.systemui (pid 390)
19440 kB: com.marakana.android.logserviceclient (pid 801)
9386 kB: android.process.acore (pid 543)
8262 kB: com.android.phone (pid 473)
6855 kB: com.android.inputmethod.latin (pid 446)
6469 kB: android.process.media (pid 439)
6198 kB: com.android.email (pid 717)
5025 kB: com.android.contacts (pid 646)
4933 kB: com.android.nfc (pid 485)
4907 kB: com.android.mms (pid 855)
4614 kB: com.android.providers.calendar (pid 692)
4519 kB: com.android.settings (pid 897)
4468 kB: com.android.calendar (pid 910)
4319 kB: com.android.exchange (pid 734)
3950 kB: com.android.deskclock (pid 631)
3153 kB: com.marakana.android.logservice (pid 495)
3033 kB: com.android.smspush (pid 565)
2944 kB: com.android.voicedialer (pid 881)
Total PSS by OOM adjustment:
33696 kB: System
33696 kB: system (pid 307)
44697 kB: Persistent
28349 kB: com.android.systemui (pid 390)
8262 kB: com.android.phone (pid 473)
4933 kB: com.android.nfc (pid 485)
3153 kB: com.marakana.android.logservice (pid 495)
19440 kB: Foreground
19440 kB: com.marakana.android.logserviceclient (pid 801)
3033 kB: Visible
3033 kB: com.android.smspush (pid 565)
6855 kB: Perceptible
6855 kB: com.android.inputmethod.latin (pid 446)
6469 kB: A Services
6469 kB: android.process.media (pid 439)
44415 kB: Home
44415 kB: com.android.launcher (pid 519)
50330 kB: Background
9386 kB: android.process.acore (pid 543)
6198 kB: com.android.email (pid 717)
5025 kB: com.android.contacts (pid 646)
4907 kB: com.android.mms (pid 855)
4614 kB: com.android.providers.calendar (pid 692)
4519 kB: com.android.settings (pid 897)
4468 kB: com.android.calendar (pid 910)
4319 kB: com.android.exchange (pid 734)
3950 kB: com.android.deskclock (pid 631)
2944 kB: com.android.voicedialer (pid 881)
Total PSS by category:
73004 kB: Dalvik
60706 kB: Other dev
25061 kB: Unknown
21758 kB: .so mmap
20943 kB: .dex mmap
5579 kB: .apk mmap
804 kB: Other mmap
714 kB: .ttf mmap
314 kB: Native
28 kB: Ashmem
12 kB: Cursor
12 kB: .jar mmap
Total PSS: 208935 kB
KSM: 0 kB saved from shared 0 kB
0 kB unshared; 0 kB volatile
|
|
We can also run dumpsys without any arguments, to get the dump of all system services. |
INFORMATION IN THIS DOCUMENT IS PROVIDED “AS IS”. NO LICENSE, EXPRESS OR IMPLIED, BY ESTOPPEL OR OTHERWISE, TO ANY INTELLECTUAL PROPERTY RIGHTS IS GRANTED BY THIS DOCUMENT. MARAKANA ASSUMES NO LIABILITY WHATSOEVER AND MARAKANA DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY, RELATING TO THIS INFORMATION INCLUDING LIABILITY OR WARRANTIES RELATING TO FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR INFRINGEMENT OF ANY PATENT, COPYRIGHT OR OTHER INTELLECTUAL PROPERTY RIGHT.
Other names and brands may be claimed as the property of others.
Copyright © 2012 Marakana Inc.
/
#